6

I would like automapper to generate URL's for a view model. For example, this is my data object:

public class User
{
  public int Id { get; set; }
  public int Name { get; set; }
}

The view model looks something like this:

public class UserListItem
{
  public string Name { get; set; }
  public string EditUrl { get; set; }
}

I would like the EditUrl property to be generated using the routes defined for the application.

Something like this:

listIten.EditUrl = Url.Action("Edit", "UserController", new { id = user.Id });

There seems to be no way to get AutoMapper to do this. There is no RequestContext, UrlHelper or anything available for mapping expressions and I haven't found any way to pass in context when invoking Mapper.Map.

Am I missing something? Or is it just a bad idea to want to do this in the first place?

Update: Additional background

I'm investigating alternative ways of generating URLs for MVC views with the aim of making ASP.NET MVC application maintenance as easy as possible. Generating URLs while mapping the viewmodels is one of the alternatives. It's easy to test and cleans up the view. It would also promote view re-usability in some cases. While trying out this idea I ran into a brick wall with AutoMapper not being able to accept any kind of (dynamic) context for a Map operation.

Marnix van Valen
  • 13,265
  • 4
  • 47
  • 74
  • @paolo No, that's not a duplicate question. I'm not asking for a way to get at the request context within a view model. It could be the start of a solution though. – Marnix van Valen Jun 21 '11 at 09:59

3 Answers3

8

I would argue this isn't AutoMapper's job.

Routing is ASP.NET specific, AutoMapper's really only good for object mapping. It has no visibility of the HTTP Context (nor should it), so it can't be done.

If you want to "re-use" this logic across multiple places, why not create a strongly-typed HTML helper?

public static MvcHtmlString EditUserLinkForModel<UserListItem>(this HtmlHelper<UserListItem> htmlHelper)
{
   var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
   return urlHelper.Action("Edit", 
                           "UserController", 
                           new { id = htmlHelper.ViewData.Model.UserId });
}

View:

@Html.EditUserLinkForModel()

Although even that's probably overkill. It's a 1 liner! :)

RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • I'd love to hear your arguments as to why this is not something AutoMapper should be able to do. I mean, applying HtmlEncode is also ASP.NET specific, but it's something that many people do with AutoMapper. Should AutoMapper not be flexible enough to allow me to generate a url for an entity? After all, generating a url is nothing more than mapping a couple of property values to a string, which is what AutoMapper is really good at. – Marnix van Valen Jun 21 '11 at 11:27
  • @Marnix - actually, it's more than "mapping a couple of property values to a string". It needs to take into account the routing table, which is only visible to a http context. If it were simply mapping to a string - you could use `opt => opt.UseValue("somestring")`. I've never seen HtmlEncode being used in auto mapper. But again, that doesn't have a reliance on the HTTP context. – RPM1984 Jun 21 '11 at 11:29
  • But again - im confused, why do you **want** automapper to do this? why is generating a URL so difficult to do in a view or a helper? – RPM1984 Jun 21 '11 at 11:31
  • @RPM1984 I want AutoMapper to do this to get cleaner code. I don't want to create a bunch of HTML helper methods, I've found that to become unmanageable very fast. Implementing this with AutoMapper seems like a really clean solution to me. As for using HtmlEncode with AutoMapper, this is advocated in [MVC 2 In Action](http://manning.com/palermo2/). – Marnix van Valen Jun 21 '11 at 11:47
  • @RPM1984 The core of my question is whether or not it is possible to supply some sort of context when applying a mapping with AutoMapper. From your answer I gather that this is not possible, right? – Marnix van Valen Jun 21 '11 at 11:50
  • AFAIK - no, you can't. however, a quick google finds this: http://stackoverflow.com/questions/5047212/how-to-use-url-content-asdf-inside-automapper-projection perhaps you can override the `Initialize` method in the controller, and setup the mapping there. – RPM1984 Jun 21 '11 at 11:59
  • I found that question too. It's about why that **doesn't** work. Setting up new mappings when the controller is created is a very bad idea. A custom resolver could work, but that would probably involve HttpContext.Current which would make unit testing difficult. – Marnix van Valen Jun 21 '11 at 12:09
  • @Marnix - you say you don't want to create a bunch of HTML helper methods, but how many different pages on your website will have a link to a particular edit page? Again - back to my question of what your trying to achieve. Logic would tell me (without knowing your website), that only 1 or 2 pages would have a given "Edit" link. I don't think that warrants refactoring. – RPM1984 Jun 21 '11 at 23:54
  • @RPM1984 - This particular app is a brownfield application being overhauled. I've added some background to the question, I hope that gives a better idea of what I'm trying to achieve. – Marnix van Valen Jun 22 '11 at 08:36
  • 1
    Accepted your answer because I think you're right. Automapper should not be (ab)used for anything other than mapping and formatting data to viewmodels. – Marnix van Valen Jul 22 '11 at 07:24
  • I'm running into this problem now where I need a url generated for me but I have no view (This is a REST API). So I can either do this in the controller or mapping configuration. – Shawn Mclean Apr 28 '12 at 20:42
  • @ShawnMclean - do it in the controller. AutoMapper doesn't have access to the HTTP context, so how can it generate URL's? – RPM1984 Apr 29 '12 at 01:15
4

Although this approach causes a host of testability issues it is possible to do what you want... utilizing HttpContext.Current.Request.RequestContext.

Mapper.CreateMap<Sample1, Sample2>().ForMember(
                destination => destination.Url, options => options.MapFrom(source => new UrlHelper(HttpContext.Current.Request.RequestContext).Content(source.Url)));

This would make testing difficult but you can get around that by injecting a class that provides the UrlHelper. Then if the URL helper can be mocked out then your testing issues are mitigated or at least the dependency on HttpContext it removed.

John Sobolewski
  • 4,512
  • 1
  • 20
  • 26
2

Just thought I'd share my findings on this topic... I've solved the issue as follows using a custom TypeConverter (AutoMapper 3.1.1):

public abstract class ObjectToUrlConverter<Source> : ITypeConverter<Source, string>
{
    public string Convert(ResolutionContext context)
    {
        UrlHelper Url = (UrlHelper)context.Options.Items["Url"];

        string result = null;

        if (context.SourceValue != null)
        {
            result = Url.Link(GetRouteName(), GetControllerName(), GetIdentifier((Source)context.SourceValue));
        }

        return result;
    }

    public abstract object GetIdentifier(Source sourceObject);

    public abstract string GetRouteName();

    public abstract string GetControllerName();
}

public class SomeEntityToUrlConverter : ObjectToUrlConverter<SomeEntity>
{
    public override object GetIdentifier(SomeEntity sourceObject)
    {
        return sourceObject.Id;
    }

    public override string GetRouteName()
    {
        return "someRouteName";
    }

    public override string GetControllerName()
    {
        return "someControllerName";
    }
}

You can subclass ObjectToUrlConverter for any object that you wish to conver to a URL. Next, create the Map for every object that you wish to convert to a URL:

Mapper.CreateMap<SomeEntity, string>().ConvertUsing<SomeEntityToUrlConverter>();

And finally, map as follows. Url is the instance of the UrlHelper.

Mapper.Map<SourceObject[], DestinationObject[]>(items, opts => opts.Items.Add("Url", Url));

Any property in SourceObject that is of type SomeEntity can now be properly converted to a destination string property, as a URL.

Korijn
  • 1,383
  • 9
  • 27