Projection using contextual values in AutoMapper
Asked Answered
G

2

7

I'm currently evaluation whether AutoMapper can be of benefit to our project. I'm working on a RESTful Web API using ASP.NET Web API, and one of the things I must return is a resource that contains links. Consider this simplified example, using the following domain object:

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

I need to map this into a resource object, sort of like a DTO but with added properties to facilitate REST. This is what my resource object may look like:

public class CustomerResource
{
    public string Name { get; set; }
    public Dictionary<string, string> Links { get; set; }
}

The Links property will need to contain links to related resources. Right now, I could construct them using the following approach:

public IEnumerable<CustomerResource> Get()
{
    Func<Customer, CustomerResource> map = customer => 
        new CustomerResource
        {
            Name = customer.Name,
            Links = new Dictionary<string, string>()
            {
                {"self", Url.Link("DefaultApi", new { controller = "Customers", name = customer.Name })}
            }
        }

    var customers = Repository.GetAll();
    return customers.Select(map);
}

...but this is pretty tedious and I have a lot of nested resources and such. The problem that I see is that I can't use AutoMapper because it doesn't let me provide certain things needed during projection that are scoped to the point where the mapping operation is performed. In this case, the Url property of the ApiController provides the UrlHelper instance that I need to create the links for me, but there may be other cases.

How would you solve this conundrum?

P.S. I typed up this code specifically for this question, and it compiled in your head but may fail in your favorite IDE.

Grampositive answered 2/4, 2013 at 14:15 Comment(5)
At the moment I'm inclined to create the map at the call site but I don't know if that's a good idea.Grampositive
When are the links defined? During runtime?Briant
To be more precise, are links defined 1) at compile time, 2) at start up time or 3) at mapping/resolve time?Briant
The Url property refers to an instance of UrlHelper instantiated per request, so at mapping time.Grampositive
I may avoid AutoMapper alltogether. It's just not a good fit for what I'm doing.Grampositive
B
2

I would look in to using a Custom Type Converter. The type converter could have contextual information injected via an IOC container. Or, since the converter is instantiated at configuration time, it could have a reference to a factory which would return contextual information each time the type converter is run.

Simple Example

You could define an interface for getting your current "context" (what that means depends on what you're doing and how you implement things so for this example I'll just the current HttpContext which gets you access to Session, Server, Items, etc...):

public interface IContextFactory
{
    HttpContext GetContext();
}

And the implementation is simply:

public class WebContextFactory : IContextFactory
{
    public HttpContext GetContext()
    {
        return HttpContext.Current;
    }
}

Your custom type converter could take an instance of IContextFactory from your IOC container and each time the mapping is run, you can call GetContext() to get the context for the current request.

Accessing the Url Property

The UrlHelper comes from the Request object attached to the current controller's context. Unfortunately, that is not available in the HttpContext. However, you could override the Initialize method on your ApiController and store the controllerContext in the HttpContext.Items collection:

protected override void Initialize(System.Web.Http.Controllers.HttpControllerContext controllerContext)
{
    HttpContext.Current.Items["controllerContext"] = controllerContext;
    base.Initialize(controllerContext);
}

You can then access that from the current HttpContext:

var helper = ((HttpControllerContext) HttpContext.Current.Items["controllerContext"]).Request.GetUrlHelper();

I'm not sure it's the best solution, but it can get you the UrlHelper instance inside your custom type mapper.

Byssinosis answered 6/4, 2013 at 12:42 Comment(7)
How would you get the context with IoC? Say you're executing Mapper.Map in a MVC controller. How would you inject the instance that you're executing in into the resolver/converter?Briant
@carlpett, I updated with a same of grabbing the current Http Context via a factory.Byssinosis
But HttpContext is not what I need when resolving.Grampositive
What is the source for the "links to related resources" you mention in your original question?Byssinosis
The UrlHelper instance that is instantiated for the request and available through the ApiController's Url property.Grampositive
Yes, that could work, even though that makes the code less testable (not that it was really testable when it referred to the ApiController's Url property)Grampositive
I was thinking the call to grab the helper from the HttpContext.Current.Items would be abstracted out via an interface (the one injected into your custom type mapper). Should make testing a little easier.Byssinosis
B
2

This is not a pretty solution, but after reading through the docs it appears that there isn't one... We're currently throwing in contextual stuff by mapping Tuple<TDomainType, TContextStuff> to TDataTransfer. So in your case you'd Mapper.CreateMap<Tuple<Customer, Controller>, CustomerResource>.

Not pretty, but it works.

Briant answered 6/4, 2013 at 14:56 Comment(1)
It's not pretty, but it's creative. I'm pondering trying this, but I'm already thinking about the mess it would create when configuring nested mappings.Grampositive

© 2022 - 2024 — McMap. All rights reserved.