4
\$\begingroup\$

I have an ever growing switch case statement I plan on adding 3 more case statements to. Given an int, compare it to an enum, and call IMapper.Map<TDestination>(sourceObject);

public WebhookActivity MakeMessage(WebhookActivityRequest request)
{
    WebhookActivity command = _mapper.Map<ConsecutiveCaseCreated>(request);
    var mappingId = 0;
    for(int i = 0; i < mappings.Length; i++)
    {
        if(request.WorkflowCode == mappings[i].WorkflowCode)
        {
            mappingId = mappings[i].EventHandlerId;
        }
    }

    switch(mappingId)
    {
        case (int)WorkflowEventHandler.CONSECUTIVE_CASE_CREATED:
            {
                command = _mapper.Map<ConsecutiveCaseCreated>(request);
                break;
            }
        case (int)WorkflowEventHandler.CONSECUTIVE_CASE_MODIFIED:
            {
                command = _mapper.Map<ConsecutiveCaseModified>(request);
                break;
            }
        case (int)WorkflowEventHandler.CONSECUTIVE_CASE_CANCELLED:
            {
                command = _mapper.Map<ConsecutiveCaseCancelled>(request);
                break;
            }
        case (int)WorkflowEventHandler.INTERMITTENT_CASE_CREATED:
            {
                command = _mapper.Map<IntermittentCaseCreated>(request);
                break;
            }
        case (int)WorkflowEventHandler.INTERMITTENT_CASE_TIME_ADJUSTED:
            {
                command = _mapper.Map<IntermittentCaseTimeAdjusted>(request);
                break;
            }
        case (int)WorkflowEventHandler.INTERMITTENT_CASE_TIME_DELETED:
            {
                command = _mapper.Map<IntermittentCaseTimeDeleted>(request);
                break;
            }
        case (int)WorkflowEventHandler.CASE_CHANGED_TYPE:
            {
                command = _mapper.Map<CaseChangedType>(request);
                break;
            }
    }

    return command;
}

I looked at other enum related questions and saw a common solution of using a dictionary. I tried that and couldn't quite figure out how to make the call to IMapper.Map() work out. Made the code a lot smaller though! Here's what the dictionary based solution I came up with looks like and what the method code became.

private static readonly Dictionary<int, Type> workflowEventHandlers = new Dictionary<int, Type>
{
    { (int)WorkflowEventHandler.CONSECUTIVE_CASE_CREATED, typeof(ConsecutiveCaseCreated)},
    { (int)WorkflowEventHandler.CONSECUTIVE_CASE_CANCELLED, typeof(ConsecutiveCaseCancelled) },
    { (int)WorkflowEventHandler.CONSECUTIVE_CASE_MODIFIED, typeof(ConsecutiveCaseModified) },
    { (int)WorkflowEventHandler.INTERMITTENT_CASE_CREATED, typeof(IntermittentCaseCreated) },
    { (int)WorkflowEventHandler.INTERMITTENT_CASE_TIME_ADJUSTED, typeof(IntermittentCaseTimeAdjusted) },
    { (int)WorkflowEventHandler.INTERMITTENT_CASE_TIME_DELETED, typeof(IntermittentCaseTimeDeleted) },
    { (int)WorkflowEventHandler.CASE_CHANGED_TYPE, typeof(CaseChangedType) }
};

public WebhookActivity MakeMessage(WebhookActivityRequest request)
{
    WebhookActivity command = _mapper.Map<ConsecutiveCaseCreated>(request);
    var mappingId = 0;
    for(int i = 0; i < mappings.Length; i++)
    {
        if(request.WorkflowCode == mappings[i].WorkflowCode)
        {
            mappingId = mappings[i].EventHandlerId;
        }
    }

    if(workflowEventHandlers.TryGetValue(mappingId, out Type mapDestinationType))
    {
        //command = _mapper.Map(request, typeof(WebhookActivityRequest), mapDestinationType);
        //command = Map(request, mapDestinationType);
    }
}
// doesn't really work
private WebhookActivityMessage Map<TSource, TDestination>(TSource source, TDestination destination) where TDestination: WebhookActivityMessage
{
    return _mapper.Map<TSource, TDestination>(source);
}

The destination types all inherit from a base class called WebhookActivityMessage which inherits from WebhookActivity so I figured if I could say the return type of my Map() method was WebhookActivityMessage then it could be happy.

\$\endgroup\$
4
  • \$\begingroup\$ Can you modify the enum? If so you could define a custom attribute to store the mapping between enum values and types. Then you could use reflection to perform the mapping lookup. \$\endgroup\$ Commented Dec 7, 2022 at 18:04
  • 1
    \$\begingroup\$ @PeterCsala I do own the enum so yes I can modify it. I do use the integer value of the enum in other places though. Your custom attribute to define the mapping sounds interesting. Wouldn't know how to implement it though. Each mapping is done in a single class extending Automapper.Profile. ex: CreateMap<WebhookActivity, ConsecutiveCaseCreated>(); \$\endgroup\$
    – Luminous
    Commented Dec 7, 2022 at 19:09
  • \$\begingroup\$ couldn't quite figure out how to make the call to IMapper.Map() => make delegates. \$\endgroup\$
    – radarbob
    Commented Dec 7, 2022 at 22:58
  • \$\begingroup\$ have you looked into Convert.ChangeType? learn.microsoft.com/en-us/dotnet/api/… \$\endgroup\$
    – bracco23
    Commented Dec 8, 2022 at 12:53

3 Answers 3

5
\$\begingroup\$

You can use a switch expression (C# 8) instead of a switch statement to make the code a lot smaller:

public WebhookActivity MakeMessage(WebhookActivityRequest request)
{
    int mappingId = 0;
    for (int i = 0; i < mappings.Length; i++) {
        if (request.WorkflowCode == mappings[i].WorkflowCode) {
            mappingId = mappings[i].EventHandlerId;
        }
    }

    return (WorkflowEventHandler)mappingId switch {
        WorkflowEventHandler.CONSECUTIVE_CASE_CREATED => _mapper.Map<ConsecutiveCaseCreated>(request),
        WorkflowEventHandler.CONSECUTIVE_CASE_MODIFIED => _mapper.Map<ConsecutiveCaseModified>(request),
        WorkflowEventHandler.CONSECUTIVE_CASE_CANCELLED => _mapper.Map<ConsecutiveCaseCancelled>(request),
        WorkflowEventHandler.INTERMITTENT_CASE_CREATED => _mapper.Map<IntermittentCaseCreated>(request),
        WorkflowEventHandler.INTERMITTENT_CASE_TIME_ADJUSTED => _mapper.Map<IntermittentCaseTimeAdjusted>(request),
        WorkflowEventHandler.INTERMITTENT_CASE_TIME_DELETED => _mapper.Map<IntermittentCaseTimeDeleted>(request),
        WorkflowEventHandler.CASE_CHANGED_TYPE => _mapper.Map<CaseChangedType>(request),
        _ => _mapper.Map<ConsecutiveCaseCreated>(request)
    };
}

But note that only the last mappingId in your for-loop will be used. Did you want to search for the first occurrence instead?

int mappingId = mappings
   .FirstOrDefault(m => request.WorkflowCode == m.WorkflowCode)?.EventHandlerId ?? 0;

As for your solution with the dictionary, you could declare a

Dictionary<WorkflowEventHandler,
            Func<IMapper, WebhookActivityRequest, WebhookActivity>>

instead. I.e., you would add it lambda expressions doing the work.

private static readonly Dictionary<WorkflowEventHandler, Func<IMapper, WebhookActivityRequest, WebhookActivity>> workflowEventHandlers = new(){
    { WorkflowEventHandler.CONSECUTIVE_CASE_CREATED, (m, r) => m.Map<ConsecutiveCaseCreated>(r) },
    { WorkflowEventHandler.CONSECUTIVE_CASE_CANCELLED, (m, r) => m.Map<ConsecutiveCaseCancelled>(r) },
    { WorkflowEventHandler.CONSECUTIVE_CASE_MODIFIED, (m, r) => m.Map<ConsecutiveCaseModified>(r) },
    { WorkflowEventHandler.INTERMITTENT_CASE_CREATED, (m, r) => m.Map<IntermittentCaseCreated>(r) },
    { WorkflowEventHandler.INTERMITTENT_CASE_TIME_ADJUSTED,(m, r) => m.Map<IntermittentCaseTimeAdjusted>(r) },
    { WorkflowEventHandler.INTERMITTENT_CASE_TIME_DELETED, (m, r) => m.Map<IntermittentCaseTimeDeleted>(r) },
    { WorkflowEventHandler.CASE_CHANGED_TYPE, (m, r) => m.Map<CaseChangedType>(r) }
};

Then you can use it like this

if (workflowEventHandlers.TryGetValue((WorkflowEventHandler)mappingId, out var mapCreator)) {
    command = mapCreator(_mapper, request);
} else {
    command = _mapper.Map<ConsecutiveCaseCreated>(request);
}
\$\endgroup\$
1
  • \$\begingroup\$ I'll mark this as accepted tomorrow in case other solutions come in. Extremely helpful in all areas! Thank you very much. \$\endgroup\$
    – Luminous
    Commented Dec 7, 2022 at 19:12
4
\$\begingroup\$

Rather than listing the mappings in a separate data structure you could use a custom attribute to decorate/annotate each enum value with the mapTo information.

enum WorkflowEventHandler
{
    [WorkflowEventHandlerMap(typeof(ConsecutiveCaseCreated))]
    CONSECUTIVE_CASE_CREATED = 1,
    
    [WorkflowEventHandlerMap(typeof(ConsecutiveCaseCancelled))]
    CONSECUTIVE_CASE_CANCELLED = 2,
    
    [WorkflowEventHandlerMap(typeof(ConsecutiveCaseModified))]
    CONSECUTIVE_CASE_MODIFIED = 3,
    
    [WorkflowEventHandlerMap(typeof(IntermittentCaseCreated))]
    INTERMITTENT_CASE_CREATED = 4,
    
    [WorkflowEventHandlerMap(typeof(IntermittentCaseTimeAdjusted))]
    INTERMITTENT_CASE_TIME_ADJUSTED = 5,
    
    [WorkflowEventHandlerMap(typeof(IntermittentCaseTimeDeleted))]
    INTERMITTENT_CASE_TIME_DELETED = 6,
    
    [WorkflowEventHandlerMap(typeof(CaseChangedType))]
    CASE_CHANGED_TYPE = 7
}

The definition of the WorkflowEventHandlerMapAttribute can be as simple as this

[AttributeUsage(AttributeTargets.Field)]
class WorkflowEventHandlerMapAttribute: Attribute
{
    public Type MapTo { get; init; }
    public WorkflowEventHandlerMapAttribute(Type MapTo) => MapTo = mapTo;
}

With reflection you can retrieve the related attribute instance like this:

var handler = WorkflowEventHandler.CONSECUTIVE_CASE_CREATED;
var attribute = typeof(WorkflowEventHandler)
    .GetField(handler.ToString())
    .GetCustomAttributes(typeof(WorkflowEventHandlerMapAttribute), false)
    .FirstOrDefault() as WorkflowEventHandlerMapAttribute;

And finally you could use the attribute.MapTo to get the ConsecutiveCaseCreated type for the above handler.


Here you can find a working example on dotnet fiddle.

\$\endgroup\$
4
\$\begingroup\$

Since you're using AutoMapper, I'm sure you've already done your mapping profiles. For that, you can just extend your work by using custom destination conversion, by using either ConvertUsing or ConstructUsing. This would be also possible since all classes are derived from WebhookActivity.

You can create a model to hold both command and WorkflowEventHandler values, then using AutoMapper just map this model and use the switch inside ConvertUsing.

AutoMapper configuration example:

public class WebhookActivityRequestModel
{
    public WebhookActivity? Source { get; set; }
    
    public WorkflowEventHandler Name { get; set; }

    public  WebhookActivityRequestModel() { }
    
    public WebhookActivityRequestModel(WebhookActivity? source, WorkflowEventHandler name)
    {
        Source = source;
        Name = name;
    }
}    
public class WorkflowEventHandlerProfile : Profile
{
    public WorkflowEventHandlerProfile() 
    {
        CreateMap<WebhookActivity, ConsecutiveCaseCreated>();
        
        CreateMap<WebhookActivity, ConsecutiveCaseModified>();

        CreateMap<WebhookActivityRequestModel, WebhookActivity>()
            .ConvertUsing((source, dest, context) =>
            {
                return source.Name switch
                {
                    WorkflowEventHandler.CONSECUTIVE_CASE_CREATED => context.Mapper.Map<ConsecutiveCaseCreated>(source.Source),
                    WorkflowEventHandler.CONSECUTIVE_CASE_MODIFIED => context.Mapper.Map<ConsecutiveCaseModified>(source.Source),
                    _ => context.Mapper.Map<ConsecutiveCaseCreated>(source.Source)
                };
            });

    }
}

Now, your method would be something like this:

public WebhookActivity MakeMessage(WebhookActivityRequest request)
{
    WebhookActivity command = _mapper.Map<ConsecutiveCaseCreated>(request);
    
    int mappingId = mappings
        .FirstOrDefault(m => request.WorkflowCode == m.WorkflowCode)?.EventHandlerId ?? 0;  
    
    if(Enum.TryParse(typeof(WorkflowEventHandler), mappingId.ToString(), out var result))
    {
        var model = new WebhookActivityRequestModel(command, result);
    
        return _mapper.Map<WebhookActivityRequestModel, WebhookActivity>(model);        
    }

    return command;
}
\$\endgroup\$
2
  • \$\begingroup\$ I like this solution for moving the actual map calls to the IMapper Profile configuration. Also my class handling WebhookActivityRequest no longer needs to know about the dictionary of delegates. \$\endgroup\$
    – Luminous
    Commented Dec 9, 2022 at 14:30
  • \$\begingroup\$ @Luminous you could also create a custom converter that implements ITypeConverter and move the switch logic inside it and then call it in the profile like ConvertUsing(new WorkflowEventHandlerConverter()) this would add a better maintainability as well. \$\endgroup\$
    – iSR5
    Commented Dec 9, 2022 at 14:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.