Flexible CSV exports over web-api with server side MEF. (part 2)

Introduction

In  a previous post I introduced the usage of MEF (the managed extensibility framework) for making the management of the CSV exports more simple.

The basic idea is to have an approach with which we can easily create a new CSV export, by means of the strictest minimum of code :

public class CustomerCSV3
    {
        public string FullName { get; set; }
        public int OrderCount { get; set; }
    }

    [Export(typeof(IProjection))]
    [ProjectionMetadata(typeof(Customer), "Customers with order count")]

    public class CustomerWithOrderCountProjection : IProjection<Customer, CustomerCSV3>
    {
        public Expression<Func<Customer, CustomerCSV3>> GetProjection()
        {
            return (Customer c) => new CustomerCSV3 { FullName = c.FirstName + " " + c.LastName, OrderCount = c.Orders.Count() };
        }
    }

 

But, … making this possible, requires quite a lot of infrastructure code. I’ll try to make this available via a Nuget package so that it’s a matter of seconds to have it available.

As you can see in the following view on my solution folder (of the server project), we really need quite some classes and interfaces:

solutionView

 

This post is about MEF, where the notation of Exports is key. Now, from a functional perspective this post is also about exporting data. This can be a source of confusion for the reader who is not familiar with MEF. The concept of Export in MEF has really nothing to do with exporting data :)

The CSV Export Controller

All request for a report will pass through one specific method in a specific controller:

using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Server;
using Microsoft.VisualStudio.ExtensibilityHosting;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Web.Http;
using Microsoft.LightSwitch.Details;

namespace LightSwitchApplication
{
    public class CSVExportController : ApiController
    {
        private static readonly MediaTypeHeaderValue _mediaType = MediaTypeHeaderValue.Parse("text/csv");

        [HttpGet]
        public HttpResponseMessage GetExport(string dataServiceName, string entitySetName, string projectionName)
        {
            IProjector projector = new Projector();

            IEnumerable data = projector.ApplySourceDataProjection(dataServiceName, entitySetName, projectionName);

            CSVExportEngine exportEngine = new CSVExportEngine();

            StringBuilder stringBuilder = exportEngine.GenerateExport(data);

            System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();

            MemoryStream memStream
                = new MemoryStream(encoding.GetBytes(stringBuilder.ToString()));
            HttpResponseMessage fullResponse = Request.CreateResponse(HttpStatusCode.OK);
            fullResponse.Content = new StreamContent(memStream);
            fullResponse.Content.Headers.ContentType = _mediaType;
            string fileName = String.Format("data-{0}.csv", DateTime.Now.ToString("yyyy-MMM-dd-HHmmss"));
            fullResponse.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("fileName") { FileName = fileName };
            return fullResponse;
        }

}

So, the GetExport method  handles a request for a specific report based on a specific projection name. We apply as good as we can seperation of concerns. The retrieval of the data which we want to export is handled by the Projector (based on IProjector) class, whereas the generation of the export itself is done by the CSVExportEngine. The rest of the controller method is boilerplate code for pushing the export to the client, simple basic web-api.

The Projector

using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Details;
using Microsoft.LightSwitch.Server;
using Microsoft.VisualStudio.ExtensibilityHosting;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Linq.Expressions;

namespace LightSwitchApplication
{
    public class Projector: IProjector
    {
        public IEnumerable ApplySourceDataProjection(
           string dataServiceName,
           string entitySetName,
           string projectionName)
        {
            using (IServerApplicationContext ctx = ServerApplicationContextFactory.CreateContext())
            {
                IDataService dataService =
                   ctx.DataWorkspace.Details.Properties[dataServiceName].Value as IDataService;
                var entitySet = dataService.Details.Properties.All()
                  .OfType<IDataServiceEntitySetProperty>()
                  .Where(n => n.Name == entitySetName).SingleOrDefault().Value;
                Type entityType = entitySet.Details.EntityType;

                var projections = VsExportProviderService.GetExports<IProjection, IProjectionMetaData>().Where(m => m.Metadata.SourceEntityType.Equals(entityType));
                if (projections == null || projections.Count() == 0)
                {
                    throw new ArgumentException("No suitable projection found");
                }

                dynamic projection
                    = projections.Where(c => c.Metadata.ProjectionName == projectionName)
                    .FirstOrDefault().Value;
                dynamic sourceQuery
                    = dataService.Details.Properties[entitySetName].Value;
                var destinationQuery
                    = DataServiceQueryable.Select(sourceQuery, projection.GetProjection()); //dyn invok
                return destinationQuery.Execute();
            }
        }
    }
}

The most important part here it the retrieval of the projection strategy based on the projection name:

VsExportProviderService.GetExports<IProjection, IProjectionMetaData>()

Since our projections are decorated with the Export attribute, they can be easily resolved. We use also specific meta data attributes:

using System;
using System.ComponentModel.Composition;
namespace LightSwitchApplication
{
    
    public interface IProjectionMetaData
    {
        string ProjectionName { get; }
        Type SourceEntityType { get; }
    }


    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class ProjectionMetadataAttribute : ExportAttribute
    {
        public ProjectionMetadataAttribute(Type sourceEntityType,string projectionName)
            : base(typeof(IProjectionMetaData))
        {
            ProjectionName = projectionName;
            SourceEntityType = sourceEntityType;
        }

        public string ProjectionName { get; set; }
        public Type SourceEntityType { get; set; }
    }
}

 

The CSV Export class

We call in our CSV export controller a specific CSV exporter:

using System;
using System.Linq;
using System.Text;
using System.Collections;

namespace LightSwitchApplication
{
    public class CSVExportEngine
    {
        public StringBuilder GenerateExport(IEnumerable data)
        {
            StringBuilder stringBuilder = new StringBuilder();
            Type itemType = data.GetType().GetGenericArguments()[0];
            string header = string.Join<string>(",", itemType.GetProperties().Select(x => x.Name));
            stringBuilder.AppendLine(header);
            foreach (var entity in data)
            {
                var vals = entity.GetType().GetProperties().Select(
                        pi => new
                        {
                            Value = pi.GetValue(entity, null)
                        });

                string _valueLine = string.Empty;

                foreach (var val in vals)
                {

                    if (val.Value != null)
                    {
                        var _val = val.Value.ToString();

                        //Check if the value contans a comma and place it in quotes if so
                        if (_val.Contains(","))
                            _val = string.Concat("\"", _val, "\"");

                        //Replace any \r or \n special characters from a new line with a space
                        if (_val.Contains("\r"))
                            _val = _val.Replace("\r", " ");
                        if (_val.Contains("\n"))
                            _val = _val.Replace("\n", " ");

                        _valueLine = string.Concat(_valueLine, _val, ",");
                    }
                    else
                    {
                        _valueLine = string.Concat(string.Empty, ",");
                    }
                }

                stringBuilder.AppendLine(_valueLine.TrimEnd(','));
            }
            return stringBuilder;
        }
    }
}

This class is really nothing special, just a simple CSV generator :)

Remember the the projections must derive from:

using Microsoft.LightSwitch;
using System;
using System.Linq.Expressions;

namespace LightSwitchApplication
{
    public interface IProjection<T, S> : IProjection
        where T : IEntityObject
        where S : class,new()
    {
        Expression<Func<T, S>> GetProjection();
    }
    public interface IProjection
    {
    }

}

Conclusion

This was the difficult part. Now, we only need to consume this functionality from the client: both the silverlight and html client.

I’ll cover that in a next post !