Given these classes:
public class Foo
{
public string Name { get; set; }
}
public class Bar
{
public string DestinationName { get; set; }
public void Alert()
{
Console.Write("Alert invoked");
}
}
I have a mapping that looks like this:
Mapper.CreateMap
.ForMember(item => item.DestinationName , property => property.MapFrom(item => item.Name ))
.AfterMap((source, destination) =>
{
destination.Alert();
});
Assume that objects of type Foo are stored in the database. Method Retrieve returns IQueryable
IQueryable
List
During the execution of given code the method Alert() is never called. Am I missing something here? How should I alter my code to call the "Alert" method?
Probably good to review how the projection feature works with AutoMapper:
Project().To() -> src/destination type to an Expression tree.
ToList() -> Expression tree to SQL to objects via your LINQ provider
AutoMapper is involved in the first step, but not in the second. I create an expression tree matching the "flattening" you've done, effectively building this for you:
var items = (from item in DB.Retrieve()
select new Bar {
DestinationName = item.Name
}).ToList()
All I'm doing is that "select" part - the projection. Because I can only create an expression tree to be used by a LINQ provider, only features in mapping that deal with strict projection can be supported for projection. Resolvers, formatters, before/after map etc. all use AutoMapper's mapping engine. The LINQ stuff uses the LINQ provider's mapping engine, for which I have no hooks to do anything custom other than the Expression tree projection, and even then, only what the LINQ provider supports in its Expression tree parsing.
tl;dr - AfterMap isn't possible because LINQ is doing the mapping and LINQ doesn't support it. I'm merely giving LINQ the mapping instructions.
Given that, if you have ideas on how to do this, I'm all ears!
No idea on how to implement this :(
@jbogard Today's AfterMap takes both the source and the destination. How about an AfterMap override (or a completely different method name, for that matter) which only takes the destination as an argument. It could work something like the following:
CreateMap<Db.Foo, Dto.Foo>()
.ForMember(f => f.StorageUrl, opt => opt.MapFrom(f => f.StorageId))
.AfterMap(f => f.StorageUrl = UrlGenerator.Instance.GenerateUrl(f.StorageUrl));
// later on:
someFooQueryable
.Where(q => q.Id > 200)
.Project()
.To<Dto.Foo>()
.ToList()
.RunAfterMap()
This could be very useful. If you get the naming conventions right it won't be as confusing as my example.
@jbogard Has there been anything further on this issue? We've run into it twice in one week on one of our projects.
I understand why the AfterMap(){} can't be run when calling .Project() but I have a thought given in your project:
AfterMap defined for a particular type mapping.Project() to perform a mapping of a specific objectWould it make sense to throw an Exception in this case? If you've specified an AfterMap() then I would assume that you expect it to be run whenever a Map is performed. If this isn't possible when using the queryable extensions then might it make better sense to fail early and so warn the developer of the issue, rather than silently completing the map, but with data possibly / likely incomplete in some way?
Cheers,
Alexander
When I say "in your project" I mean to say "Given the following conditions in a piece of code".
I like the alternate approach.
The issue is that there are a number of features that don't work in
projection, but could work on their own. I work on projects where we do
both - projections and regular maps. A runtime exception wouldn't be
appropriate, you can't easily turn it off or ignore it.
On Fri, Aug 29, 2014 at 3:50 AM, Alexander Harris [email protected]
wrote:
When I say "in your project" I mean to say "Given the following conditions
in a piece of code".—
Reply to this email directly or view it on GitHub
https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-53851890
.
I take your point about not being able to turn off an Exception, but what I had in mind was throwing if:
That is assuming that the extensions are able to examine the mappings at the point the call id add and determine if an AfterMap is present.
Calling Mapper.Map would not be affected obviously.
This is what I had in mind.
What about all the other non-supported methods? ResolveUsing? Condition? etc
On Fri, Aug 29, 2014 at 2:00 PM, Alexander Harris [email protected]
wrote:
I take your point about not being able to turn off an Exception, but what
I
had in mind was throwing if:
- The caller called the Project extension method.
- The type pair being mapped / projected from / to contained an aftermap.
That is assuming that the extensions are able to examine the mappings at
the point the call id add and determine if an AfterMap is present.Calling Mapper.Map would jot he affected , obviously.
This is what I had in mind.
On 29 Aug 2014 15:50, "Jimmy Bogard" [email protected] wrote:I like the alternate approach.
The issue is that there are a number of features that don't work in
projection, but could work on their own. I work on projects where we do
both - projections and regular maps. A runtime exception wouldn't be
appropriate, you can't easily turn it off or ignore it.On Fri, Aug 29, 2014 at 3:50 AM, Alexander Harris <
[email protected]>
wrote:When I say "in your project" I mean to say "Given the following
conditions
in a piece of code".—
Reply to this email directly or view it on GitHub
<https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-53851890>
.
—
Reply to this email directly or view it on GitHub
<
https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-53885783>.
—
Reply to this email directly or view it on GitHub
https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-53917062
.
For the case where you want to do both projections and regular maps, you could add a third argument to the .AfterMap() call:
mapper.CreateMap<Db.Foo, Dto.Foo>
.AfterMap((source, dest) => source.Property = dest.SomeOtherProperty, MapMode.IgnoreOnProjection)
.AfterMap(dest => dest.Foo += 10)
The same argument would apply to the other non-supported methods.
Something like that sounds like an interesting idea, although not sure if it's possible since the projection stuff is part of an extension, so a different library?
It would at least give users an "opt out". The exception message could then become something like:
AfterMap is not supported when using projection and would be ignored. If this is the intended behaviour, add the MapMode.IgnoreOnProjection to the AfterMap call.
With "AfterMap" replaced with e name of the non supported function
As for those others, I've not used them myself so can't really comment, but I assume they work in a similar fashion, or rather suffer the same issue?
Should I just update the documentation?
https://github.com/AutoMapper/AutoMapper/wiki/Queryable-Extensions
:+1: to @vegardlarsen's idea. An AfterMap that doesn't participate in projection would be a great addition.
@mrchief AfterMap already doesn't participate in projection. You can just call Mapper.Map.
It doesn't? The documentation suggests otherwise.
Not sure what you mean by:
You can just call Mapper.Map.
Do you mean this will generate the same query expression as Project().To<Dto.Foo>()
but without invoking AfterMap?
someFooQueryable
.Where(q => q.Id > 200)
.Select(Mapper.Map<Dto.Foo>)
.ToList()
If you want AfterMap call Mapper.Map instead of doing projections. That's
all.
On Monday, July 6, 2015, Hrusikesh Panda [email protected] wrote:
It doesn't? The documentation
https://github.com/AutoMapper/AutoMapper/wiki/Queryable-Extensions#supported-mapping-options
suggests otherwise.Not sure what you mean by:
You can just call Mapper.Map.
Do you mean this will generate the same query expression as
Project().To()
but without invoking AfterMap?someFooQueryable
.Where(q => q.Id > 200)
.Select(Mapper.Map)
.ToList()—
Reply to this email directly or view it on GitHub
https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-119002177
.
Yeah well, I want Projections too! :)
That's really not possible. All AutoMapper does is pass a Select expression to the query provider. Unless the query provider has some sort of "after query" hook, I've got nothing, AutoMapper takes this:
dbContext.Products.Where(p => p.Id == id)
.Select(p => new ProductDto {
Name = p.Name,
Id = p.Id
})
.ToList();
And replaces it with this:
dbContext.Products.Where(p => p.Id == id)
.ProjectTo<ProductDto>().ToList();
Where exactly would AfterMap go? Not between ProjectTo and ToList - I'm still working off of an IQueryable, which gets passed to a query provider to translate an expression tree into an IEnumerable. A different ToList()? Sure, you can do this yourself:
dbContext.Products.Where(p => p.Id == id)
.ProjectTo<ProductDto>().ToList()
.Select(dto => { dto.Foo += 10; return dto }).ToList();
That's the recommendation at this point. It's impossible as far as I can see to support AfterMap directly in an arbitrary IQueryable. So just do it yourself!
I hear you. My expression tree foo is not strong enough and I don't even know if this is possible programmatically (perhaps not) but I wanted to run this by you anyway, just to see if this sparks up anything.
I was hoping that Automapper has enough info to build this expression:
dbContext.Products.Where(p => p.Id == id)
.ProjectTo<ProductDto>().ToList() // <= this comes from normal map
.Select(dto => { dto.Foo += 10; return dto }).ToList(); // <= this comes from AfterMap
Sure I can do it myself, but with AutoMapper, I have all the mapping logic in one place. With DIY, it gets scattered and repeated.
Here's the problem, on who should do this mapping:
.Products // EF
.Where(p => p.Id == id) // EF/LINQ/Queryable
.ProjectTo<ProductDto>() // AutoMapper
.ToList() // EF
.Select(dto => { dto.Foo += 10; return dto }) // AutoMapper?
.ToList(); // Not a queryable, but an enumerable
Ignore expression trees, given the above pipeline, how would this work, considering there's that very important middleman?
In order to support this, you'd have to replace ToList() and all of its ilk that invoke the queryable.
Alternatively, you can put readonly computed properties in your model, or parameterize your AutoMapper config instead of AfterMap.
Hmmm... I see the problem now.
As a matter of fact, I'm using computed properties (I make them internal and annotate them as such, just to make it explicit that they exist for the sole purpose of supporting projections + afterMap scenarios), but they feel more like cruft than a real solution.
I have another idea though but before I waste any more of your time, I want to tinker with it and see if its viable. Let me go try it and I'll update you with my findings.
So this might help with projecting computed properties:
Yeah, seen that too! Another good post! While that works for simple computations, I've ran into cases where I need to do more along these lines .AfterMap(f => f.StorageUrl = UrlGenerator.Instance.GenerateUrl(f.StorageUrl));.
Ah yes, accessing something outside the context of anything on that entity. Can't help you unfortunately, SQL database knows nothing about UrlGenerator.
I'm totally open to ideas, I just haven't found any viable solution.
Could I propose an idea to resolve this?
In ProjectionExpression.To instead of return the IQueryable<T> created by original Provider, it should returns a wrapper around it. Every member implementation should just call the original, but each time you return an IQueryable<T> you need to ensure to return your wrapper. When IEnumerable is called, you call the original EF IEnumerable implementation and call the AfterMap method.
There is a limitation: if you call ProjectTo and after you create another projection IQueryable<T2>, the AfterMap cannot be called
So the result could only work off of the destination type, I don't think that would solve this problem.
It works if ProjectTo is the final projection. Better than nothing :smile:
I just bumped into a similar circumstance. Let's say I have a model Alpha, and a view model AlphaViewModel.
For single item mappings, no problem, the following after map works fine.
``` C#
private void AlphaAfterMap(Alpha a, AlphaViewModel viewModel)
{
// do some stuff with a and/or viewModel
}
However, when I have a list of Alphas, the after map gets ignored.
I setup with a mapping options provider:
``` C#
private void MappingOptionsProvider(IMappingOperationOptions<IList<Alpha>, IList<AlphaViewModel>> opts)
{
opts.AfterMap((a, b) =>
{
var aEnum = a.GetEnumerator();
var bEnum = b.GetEnumerator();
while (aEnum.MoveNext() && bEnum.MoveNext())
AlphaAfterMap(aEnum.Current, bEnum.Current);
});
}
And which I half expect to be called during the list mapping.
``` C#
IList
var viewModels = _mapper.Map
alphas, MappingOptionsProvider).ToList();
```
However, does not seem to be working; the opts are filled in, but the AfterMap function does not get called.
I suppose I could Zip up the alphas and the viewModels and work with them that way, but if I can have it happen during the mapping, just as well.
you can do something like alphas.Select(_mapper.Map<AlphaViewModel>).ToList() and that should call the after maps.
There's no real reason to map lists explicitly. Automapper handles those situations for you, if it's 2 properties your are trying to map together.
@TylerCarlson1 That's a good point; thanks.
I'm currently using "helper" properties to hang the results of the projections and rely on getters to resolve the final values before returning results.
Wondering if this "helper" approach could be added a runtime via AutoMapper (Proxy?) and use a new helper method (e.g. AfterProjection) so I don't actually have to pollute my POCOs:
CreateMap<From, To>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.OrderId)
.ForMember(d => d.ProjectionHelper, o => o.MapFrom(s => s.Order.Detail.FirstOrDefault())
.AfterProjection(d => d.CalculatedProperty, src => src.ProjectionHelper.SomethingTricky());
or
CreateMap<From, To>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.OrderId)
.ForMember(d => d.ProjectionHelper, o => o.MapFrom(s => s.Order.Detail.FirstOrDefault())
.AfterProjection(d => d.CalculatedProperty, src => src.ProjectionHelper.SomeTrickyProperty);
@brettveenstra That's an interesting approach, but in my case I also depend upon HTTP request IdentityUser details. So I end up providing a helper method in the context of my MVC Controller(s). Works like a champ.
@mwpowellhtx I've been using the Parameterization support for those situations ...
From the example:
string currentUserName = null; // or your IdentityUser
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName));
dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUserName = Request.User.Name });
For the cases I'm concerned with, it's mostly where there is some additional non-DB behavior that exists in the EF Model (leaky abstraction I know) and/or the generated SQL is so repetitive/slow that it's an operation best done with a "just get me this and I'll do the rest in code" mentality.
For now, I'm left creating an override to the extension methods of AutoMapper.QueryableExtensions and also the added cruft to my POCOs for those "helpers".
@brettveenstra If it was _just_ a string user name, it would be one thing, but it involves a first class IUser instance, along with some Claims details. Appreciate the feedback, though.
@mwpowellhtx I didn't realize AutoMapper Parameterization doesn't support complex types. Something like this doesn't already work?
// mapping
ClaimsIdentity currentUser = null;
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUser.Name));
// usage
var currentIdentity = (ClaimsIdentity)HttpContext.User;
dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUser = currentIdentity });
for claims-specific operations, you might have to do...
// mapping
ClaimsIdentity currentUser = null;
bool adminRole = false;
cfg.CreateMap<Course, CourseModel>()
.ForMember(m => m.AdminInfo, opt => opt.MapFrom(src => src.AdminItems.First(f => adminRole)))
.ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUser.Name));
// usage
var currentIdentity = (ClaimsIdentity)HttpContext.User;
dbContext.Courses.ProjectTo<CourseModel>(Config, new { currentUser = currentIdentity, adminRole = currentIdentity.IsInRole("Admin") });
What you're describing feels more like criteria rather than transformative issue. Don't mean to misrepresent your needs, just feels like it's off topic to this specific issue.
@brettveenstra
Don't take this personally, but I'm not asking for a solution to a problem I've already solved. I am using custom claims. Still, the point is well taken.
Agreed, I'm not sure how we went down this rabbit hole.
Thank you...
@mwpowellhtx - none taken dude ... Just trying to understand your usage/need around projection after queryable :)
Here's what I've done in the meantime (again you need some field to reference upon the initial mapping):
public static async Task<List<TDestination>> ProjectToListAsync<TDestination>(this IQueryable queryable,
IConfigurationProvider config, Action<TDestination> postAction)
{
var projectToListAsync =
await queryable.ProjectTo(config, new Expression<Func<TDestination, object>>[0]).DecompileAsync().ToListAsync();
if (projectToListAsync.Any())
{
projectToListAsync.ForEach(postAction);
}
return projectToListAsync;
}
HTH
@brettveenstra This is what I would recommend in any case. Make your own extension methods, because I don't want to put AutoMapper in the business of doing ToListAsync() - since you've also included the DecompileAsync method in there.
@jbogard Ok ... I just pulled this from EntityFrameworkExtensions.cs and tweaked. Would you want to see a PR on this?
On what, AutoMapper.EF6?
@brettveenstra Ah, well, that's the clincher. Suggest separate issue for that, since I am not using EF, but rather Fluent NHibernate. If you can provide a repository agnostic extension, or on a data layer specific repository, would be better, IMO, two cents.
@jbogard yep AutoMapper.EF6
@mwpowellhtx could you post some more code ... around the call to .ProjectTo?
@brettveenstra As per previous post on April 5, already done; problem solved.
I guess we were curious what your solution looked like!
On Wednesday, April 20, 2016, Michael Powell [email protected]
wrote:
@brettveenstra https://github.com/brettveenstra As per previous post on
April 5, already done; problem solved.—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/AutoMapper/AutoMapper/issues/415#issuecomment-212556237
@jbogard And I responded.
@brettveenstra Can you provide an example function for your postAction parameter in your projectToAsync extension? see above
I think this can solve my issue where I need to perform an AfterMap call to generate a specific Type depending on the result of my source parameter.
public class TransactionDTO: Entity<long, bool>, IMatter
{
public TransactionDTO(): base(new long(), true)
{
//parameterless constructur is required for EF.
}
public TransactionDTO(long key): base(key, true)
{
}
public IStatus Status
{
get; set;
}
//Initial Plan was to return to AfterMap an IStatus implemented Type.
public IStatus GenerateStatus()
{
throw new NotImplementedException();
}
//But then thought I'd have to set d.Status instead.
internal void GenerateStatus(Data.Transaction s, TransactionDTO d)
{
//If Condition to determine what IStatus is appropriate.
d.Status = new TransactionStatus { Key = 123, Name = "Testing Status" };
}
}
//---------------------------------------------------------------------------------------------------------
Mapper.Initialize(cfg => {
cfg.CreateMap<Data.Transaction, TransactionDTO>()
.AfterMap((s, d) => d.GenerateStatus(s, d))
.ForMember(dest => dest.Status, opts => opts.Ignore());
});
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.