Caffeine-Powered Life

Reports in MVC, Part 2

My last post was on creating Crystal Reports with MVC. It worked, and it was very fast to implement. However, there were a few problems with it. Let’s review the code as originally posted.


public class ReportsController : Controller

{

  private readonly ReportSampleDataContext _db = new ReportSampleDataContext();



  public ActionResult SalesYtd()

  {

    var reportPath = Server.MapPath("~/ReportTemplates/SalesYTD.rpt");

    var customers = _db.Customers;



    using (var reportDocument = new ReportDocument())

    {

      reportDocument.Load(reportPath);

      reportDocument.SetDataSource(customers);



      var response = System.Web.HttpContext.Current.Response;

      reportDocument.ExportToHttpResponse(ExportFormatType.PortableDocFormat, response, true, "SalesYTD");

    }

    return new EmptyResult();

  }

}

First off, you couldn’t unit test the controller action. That was because of the following runtime dependencies.

  • We are referencing our database directly through our DataContext object. We should abstract away the DataContext and work with some sort of repository pattern. Anything with an interface that can be injected and mocked will do.
  • We are relying on the runtime server by using Server.MapPath(). That method call will fail during a unit test.

We’re not too worried about these two. They’re easy to fix, and there’s nothing in .NET that can’t be solved by adding a layer of abstraction. In the example below, we’ve moved the data and the path resolution to an IReportingService interface. This can be injected and mocked. When you write your unit test, your implementation of MapPath should return the report document in relationship to your unit test executable.


public class ReportsController : Controller

{

  private readonly IReportingService _service;



  public ReportsController(IReportingService service)

  {

    _service = service;

  }



  public ActionResult SalesYtd()

  {

    var reportPath = _service.MapPath("~/ReportTemplates/SalesYTD.rpt");

    var customers = _service.GetCustomers();



    using (var reportDocument = new ReportDocument())

    {

      reportDocument.Load(reportPath);

      reportDocument.SetDataSource(customers);



      return new CrystalReportResult(reportDocument, "SalesYTD");                

    }            

  }

}  

The last new part is the CrystalReportResult. This will clean up the return portion of your code, keeping the actual report generation DRY and consistent.


public class CrystalReportResult : ActionResult

{

  private readonly string _attachmentName;

  private readonly ReportDocument _reportDocument;



  public CrystalReportResult(ReportDocument reportDocument, string attachmentName)

  {

    _reportDocument = reportDocument;

    _attachmentName = attachmentName;

  }



  public override void ExecuteResult(ControllerContext context)

  {

    _reportDocument.ExportToHttpResponse(ExportFormatType.PortableDocFormat, HttpContext.Current.Response, true, _attachmentName);

  }

}  

There is one last optimization to worry about. You will still have a reference to Crystal Reports in your controllers. This hasn’t been a big deal for me in the past. You could add one more layer of abstraction and render the report directly to a byte[]. From there, you would return a standard MVC FileStreamResult as an attachment. This would break the dependency on Crystal in your controllers. On your report document instance, check out the ExportToStream() method. That should be all you need to get going with that solution.

Comments