3
\$\begingroup\$

I have an ASP.NET MVC 5 web application using the repository pattern, and I have several controllers that need to call my _loggingService (queries audit logs) to get the last updated information for certain pages, not all pages.

I want to abstract this method into a common place, so that I don't have duplicated code in each controller where it is used.

I don't want to use the base controller, because I don't want to have to inject my service dependency into every controller, because it isn't used on a lot of controllers.

I don't want to make a common static class in my Application Layer, because I want to test it, and I want to avoid a static implementation.

I can't put it in another project, because it will create a circular reference to my Web Application Project because it is populating the LastUpdatedViewModel, which is an object on several other viewmodels in the project.

How do I architect this?

HttpGet for an EditPage:

[HttpGet]
public virtual ActionResult Edit()
{
    var settings = settingsService.GetSettingsForType(SettingTypeEnum.Application);

    var viewModel = new AppSettingsEditViewModel
    {
        KeyBitLengthList = GetKeyBitLengthListItems(),
        LastUpdatedViewModel = GetLastUpdated(Url.Action(MVC.ApplicationSettings.Edit(), Request.Url.Scheme, Request.Url.Host)),
        .......
    }
}

GetLastUpdated is the method I want to extract. Here it is:

public LastUpdatedViewModel GetLastUpdated(string url)
{
    var auditLog = _loggingService.GetMostRecentAudit(url);
    if (auditLog != null)
    {
        var lastUpdatedViewModel = new LastUpdatedViewModel
        {
            LastUpdated = String.Format("{0} {1} {2}", "Last Updated:", auditLog.UserAccountEmail, auditLog.CreatedDate)
        };
        return lastUpdatedViewModel;
    }
    return null
}   

BONUS POINTS for whomever can give me an elegant solution on how to get Request.Url.Scheme and Request.Url.Host mocked, and implemented in Unit Tests for our controllers, as we DO currently have unit tests on those. I'd also like to get this code tested and passing. Currently, my only solution to get tests passing was to just check for nulls on Request and Request.Url, and if either of those were null, return null in GetLastUpdated(), like so:

private LastUpdatedViewModel GetLastUpdated()
{
    if (Request == null) return null;
    if (Request.Url == null) return null;
    var url = Url.Action(MVC.EmailTemplate.EditSmtpSettings(), Request.Url.Scheme, Request.Url.Host);
    var auditLog = _loggingService.GetMostRecentAudit(url);
    if (auditLog != null)
    {
        var lastUpdatedViewModel = new LastUpdatedViewModel
        {
            LastUpdated = String.Format("{0} {1} {2}", "Last Updated:", auditLog.UserAccountEmail, auditLog.CreatedDate)
        };
        return lastUpdatedViewModel;
    }
    return null;
}

Which isn't a solution at all, only working code. What I need is a dynamic method that accepts a string URL parameter generated from a controller using T4MVC, that calls the service layer and builds my LastUpdatedViewModel.

Here is a controller test that fails because Request is null:

[Test] public void EditGET_ModalityKeyBitLengthSettingInList_ViewModelModalityKeyBitLengthEqual()
{
    var appSettings = new List<Setting>
    {
        new Setting
        {
            Key = SettingsConstants.ModalityKeyBitLength,
            Value = "1024"
        }
    };

    AutoMocker.Get<ISettingsService>()
        .Stub(x => x.GetSettingsForType(SettingTypeEnum.Application))
        .Return(appSettings);

    AutoMocker.ClassUnderTest.ControllerContext = MockControllerContext();

    var result = AutoMocker.ClassUnderTest.Edit() as ViewResult;
    var viewModel = result.Model as AppSettingsEditViewModel;

    Assert.AreEqual(1024, viewModel.Settings.ModalityKeyBitLength);
}

protected ControllerContext MockControllerContext()
{
    var context = MockRepository.GenerateStub<ControllerContext>();
    var httpContext = MockRepository.GenerateStub<HttpContextBase>();

    var session = MockRepository.GenerateStub<HttpSessionStateBase>();
    httpContext.Stub(x => x.Session).Return(session);

    httpContext.Expect(x => x.Cache).Return(HttpRuntime.Cache).Repeat.Any();

    context.HttpContext = httpContext;
    return context;
}

What I had done in another area of the application that used T4MVC was to just rip T4MVC out, and I added const strings in the service layer for URLs that we needed when we were building emails to send in our service layer.

Is T4MVC able to be implemented elegantly? The more I use it the more it just seems like a problem rather than a solution. But that's probably because I'm doing it wrong.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Well, my first attempt is to suggest injecting a mapper into the controllers you want to do this for (assuming you are using a DI framework. If not anyone of the popular choices out there will meet your needs. AutoMapper, Unity, Autofaq etc).

public interface IMap<TTarget, TSource> 
    where TTarget: class
    where TSource: class
{
    TTarget Map(TSource source);
}

public interface ILoggingServiceAdapter : IMap<TTarget, TSource>
{
   // any other items you wish to expose from your logging service
}

public class LoggingServiceMapper : ILoggingServiceAdapter 
{   
    private readonly LoggingService _loggingService;
    public LoggingServiceMapper(LoggingService loggingService)
    {
        _loggingService = loggingService;
    }

    public LastUpdatedViewModel Map(string url)
    {
        var auditLog = _loggingService.GetMostRecentAudit(url);
        if (auditLog != null)
        {
            var lastUpdatedViewModel = new LastUpdatedViewModel
            {
                LastUpdated = String.Format("{0} {1} {2}", "Last Updated:", auditLog.UserAccountEmail, auditLog.CreatedDate)
            };
            return lastUpdatedViewModel;
        }
        return null;
    }
}

Hence your methods would now look like

private readonly ILoggingServiceAdapter _loggingService;

public MyControllerConstructor(ILoggingServiceAdapter loggingService)
{
    _loggingService = loggingService;
}
[HttpGet]
public virtual ActionResult Edit()
{
    var settings = settingsService.GetSettingsForType(SettingTypeEnum.Application);

    var viewModel = new AppSettingsEditViewModel
    {
        KeyBitLengthList = GetKeyBitLengthListItems(),
        LastUpdatedViewModel = _loggingService.Map(Url.Action(
                                                     MVC.ApplicationSettings.Edit(),
                                                     Request.Url.Scheme,
                                                     Request.Url.Host)),
        .......
    }
}

Now I don't know about the BONUS option, but if you wanted to further eliminate the need to always be specifying the Scheme and Host when Creating the action you could consider writing an extension class on UrlHelper.

public static class UrlHelperExtensions
{
    public static string Action(this UrlHelper urlHelper, string action, Uri uri) {
        return urlHelper.Action(action, uri.Scheme, uri.Host);
    }
}

Hence your code now becomes like:

[HttpGet]
public virtual ActionResult Edit()
{
    var settings = settingsService.GetSettingsForType(SettingTypeEnum.Application);

    var viewModel = new AppSettingsEditViewModel
    {
        KeyBitLengthList = GetKeyBitLengthListItems(),
        LastUpdatedViewModel = _loggingService.Map(Url.Action(
                                                     MVC.ApplicationSettings.Edit(),
                                                     Request.Url)),
        .......
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ Great suggestion. I really like it, thank you for taking the time to reply. I would love to implement it, but I'd rather not implement AutoMapper, instead I would rather write my own custom mapping utilities. I'm assuming TTarget, TSource are AutoMapper provided classes? \$\endgroup\$
    – BeeTee2
    Feb 28, 2014 at 2:56
  • \$\begingroup\$ No, this is just your own interface to work behind a mapping layer. TTarget and TSource are just any generic class. This then might allow you to inject mapping services if you wished into your controllers. \$\endgroup\$
    – dreza
    Feb 28, 2014 at 3:06
  • \$\begingroup\$ What's supposed to be in the adapter in your example? Your edit method is still calling the Logging Service directly... Am I missing something here? \$\endgroup\$
    – BeeTee2
    Feb 28, 2014 at 4:06
  • \$\begingroup\$ Sorry, no the _loggingService is an implementation of the ILoggingServiceAdapter interface so it's calling the loggingservice itself indirectly through this interface/class \$\endgroup\$
    – dreza
    Feb 28, 2014 at 4:16
  • \$\begingroup\$ This put me on the right track, architecture-wise, although I ended up doing a few things a little differently. So marking your answer as accepted. Thank you. \$\endgroup\$
    – BeeTee2
    Feb 28, 2014 at 16:58

Your Answer

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

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