Showing posts with label Code Generation. Show all posts
Showing posts with label Code Generation. Show all posts

Tuesday, November 17, 2015

T4 Generates my business objects for me revisited with F#

A long time ago I wrote an article about the code I was using in T4 Generates my business objects for me. Today I've rewritten the idea with F# as the target. F# projects don't support T4 so I used a C# project to generate F# code into an F# project. The parts of the generated code
  • A readonly interface
  • A read-write interface
  • An F# record type
  • An F# module for mapping code methods (with Statically Resolved Type Parameter support aka Duck Typing or Structural Typing)
  • An F# class with INotifyPropertyChanged for WPF consumption
Benefits
  • Implementing interfaces is not allowed to be implicit so I've automated that headache.
  • INotifyPropertyChanged desires magic strings (exception in the lovely new C# 6 nameof operator) so automation makes sure the strings are correct as of the last time T4 was run
  • If I pass around the read-only interface, I don't care how far down the rabbit hole of that object goes, it's immutable, no one is modifying it
  • If I pass around the read-write interface, all the consuming methods can be ignorant of concrete types
  • I'm generating Metadata from the db into the comments on the properties (tons of untapped potential here)

View the generated code and generator t4

- Generator and generated code from new T4 to F#

Things I want to add:

  • foreign key information to the auto-generated property comments
  • consider having the Read-Write interface implement the Read-Only interface
  • figure out how to update Microsoft's Type Providers to have them implement my interfaces.
  • add an option for generating Sql Sprocs that work against these types

Thursday, April 17, 2014

T4 ApiController Generator

This T4 assumes your DbContext is called ApplicationDbContext in a file called ApplicationDbContext.cs and does not account for whatever custom namespaces need to be imported for this file for your use. It is set up for DI, but lacks consumption of an IRepository pattern or anything similar. It also expects to be in a directory below the EnvDteHelper.ttinclude.
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Data.Entity.Design.PluralizationServices" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<# DTE Dte;#>
<#@ include file="../EnvDteHelper.ttinclude"#>
<# 
  var suggestedNs=System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint");
  var projects = RecurseSolutionProjects(Dte);
  var q= from p in projects
      from pi in p.ProjectItems.Cast<ProjectItem>()
      where pi.FileCodeModel!=null
      select new{p,pi,CodeElements=pi.FileCodeModel.CodeElements.Cast<CodeElement>()};
  var context= q.Where(x=>x.pi.Name=="ApplicationDbContext.cs").First();
  var typesToMap = Descendants(context.CodeElements,ce=>ce.Children.Cast<CodeElement>()).OfType<CodeProperty>();
  var project=GetProjectContainingT4File(Dte,false);
  var pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));
#>
using System.Linq;
using System.Web.Http.OData;

using Microsoft.AspNet.Identity.EntityFramework;

namespace <#=suggestedNs#>
{
<#  foreach(var tm in typesToMap){
  var singular=pluralizationService.Singularize(tm.Name);
  var plural=pluralizationService.Pluralize(tm.Name);
#>

    public class <#=tm.Name#>Controller : ODataController
    {
        readonly ApplicationIdentityContext _db;

        public <#=tm.Name#>Controller(ApplicationIdentityContext db)
        {
            _db = db;
        }

        /// GET api/<#=singular#>
        public IQueryable<<#=singular#>> Get()
        {
            return _db.<#=plural#>;
        }

        // GET api/<#=singular#>/5
        public <#=singular#> Get(int id)
        {
            return _db.<#=plural#>.FirstOrDefault(x=>x.<#=singular#>Id==id);
        }

    }
<#}#>
}

Wednesday, February 6, 2013

AutoScaffold Index.cshtml tabular view site-wide

I was tired of generating the index.cshtml list over and over again everytime the model or viewmodel changes. So how could I get that to be runtime generated? Enter Haacked's article on Mvc Tabular Display Template Updated to work with Razor and with IEnumerable First the DisplayTemplate
@using System
@using MyMvc.Models
@model IEnumerable<dynamic>
@{
    //http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx
    var items = Model.ToArray();
    var metadata = ModelMetadata.FromLambdaExpression(m => m.ToArray()[0], ViewData);
    var properties = metadata.Properties.Where(pm => TableViewHelper.ShouldShow(pm, ViewData)).OrderBy(pm => pm.Order);
}

<table>
    <thead>
        <tr>
            @foreach (var property in properties)
            {
                <th>@property.GetDisplayName()</th>
            }
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            var itemMetadata = ModelMetadata.FromLambdaExpression(m => item, ViewData);
            <tr>
               
                @foreach (var property in properties)
                {
                    var propertyMetdata = itemMetadata.Properties.Single(m => m.PropertyName == property.PropertyName);
                    <td>
                        @Html.DisplayFor(m => propertyMetdata.Model)
                    </td>
                }
            </tr>
        }
    </tbody>
    
</table>

<span>Showing @Model.Count() items</span>
Then the helper
@using System
@using MyMvc.Models
@model IEnumerable<dynamic>
@{
    //http://haacked.com/archive/2010/05/05/asp-net-mvc-tabular-display-template.aspx
    var items = Model.ToArray();
    var metadata = ModelMetadata.FromLambdaExpression(m => m.ToArray()[0], ViewData);
    var properties = metadata.Properties.Where(pm => TableViewHelper.ShouldShow(pm, ViewData)).OrderBy(pm => pm.Order);
}

<table>
    <thead>
        <tr>
            @foreach (var property in properties)
            {
                <th>@property.GetDisplayName()</th>
            }
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            var itemMetadata = ModelMetadata.FromLambdaExpression(m => item, ViewData);
            <tr>
               
                @foreach (var property in properties)
                {
                    var propertyMetdata = itemMetadata.Properties.Single(m => m.PropertyName == property.PropertyName);
                    <td>
                        @Html.DisplayFor(m => propertyMetdata.Model)
                    </td>
                }
            </tr>
        }
    </tbody>
    
</table>

<span>Showing @Model.Count() items</span>
Then add a Views/Shared/Index.cshtml
@model System.Collections.Generic.IEnumerable<dynamic>
@{
    ViewBag.Title = "Index";
}

@Html.DisplayForModel("Table")

Change your Mvc AddView template

I didn't like that the AddView template ignored the DisplayAttribute(Order=0). So I adjusted it following http://blogs.msdn.com/b/webdev/archive/2009/01/29/t4-templates-a-quick-start-guide-for-asp-net-mvc-developers.aspx I added the following to my CodeTemplates/AddView/CSHTML/List.tt file
IEnumerable<PropertyInfo> SortProperties(IEnumerable<PropertyInfo> props)
    {
      var pl = props.ToList();
      var length = pl.Count;
      var q = from p in pl
            let da = p.GetCustomAttribute<DisplayAttribute>()
            where da != null
            let o = da.GetOrder()
            where o.HasValue
            orderby o
            select new { p, o.Value };

      var ordered = q.ToList();
      var remainder = pl.Except(ordered.Select(o => o.p).ToArray()).ToArray();
      var destination = new PropertyInfo[length];

      foreach (var o in ordered)
      {
        destination[o.Value] = o.p;
      }

      if (ordered.Count == length)
        return ordered.Select(o => o.p).ToArray();

      var remainderCount = 0;
      for (int i = 0; i < pl.Count; i++)
      {
        if (destination[i] != null)
          continue;
        destination[i] = remainder[remainderCount];
        remainderCount++;

      }
      return destination;


    }
then updated GetEligibleProperties to call it in the property loop. Here's the unit test:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestTools.UnitTesting;


namespace MyMvc.Tests.CodeTemplates.AddView.CSHTML
{
  [TestClass]
  public class ListTests
  {
    class SortTester
    {
      public Guid FileGuid { get; set; }

      [Display(Order = 0)]
      public string FileName { get; set; }

      public int CustomerId { get; set; }

      [Display(Order = 1)]
      public int? ExpectedTrxCount { get; set; }
    }
    IEnumerable<PropertyInfo> SortProperties(IEnumerable<PropertyInfo> props)
    {
      var pl = props.ToList();
      var length = pl.Count;
      var q = from p in pl
            let da = p.GetCustomAttribute<DisplayAttribute>()
            where da != null
            let o = da.GetOrder()
            where o.HasValue
            orderby o
            select new { p, o.Value };

      var ordered = q.ToList();
      var remainder = pl.Except(ordered.Select(o => o.p).ToArray()).ToArray();
      var destination = new PropertyInfo[length];

      foreach (var o in ordered)
      {
        Console.WriteLine("ordering:" + o.p.Name);
        destination[o.Value] = o.p;
      }

      if (ordered.Count == length)
        return ordered.Select(o => o.p).ToArray();

      var remainderCount = 0;
      for (int i = 0; i < pl.Count; i++)
      {
        if (destination[i] != null)
          continue;
        destination[i] = remainder[remainderCount];
        remainderCount++;

      }
      foreach (var d in destination) Console.WriteLine(d.Name);
      return destination;


    }
    [TestMethod]
    public void Sort_DisplayOne_IsSecond()
    {
      var type = typeof(SortTester);
      
      var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
      var expected = props.First(p => p.Name == LinqOp.PropertyOf(()=>new SortTester().FileName).Name);
      var sorted = SortProperties(props);
      var actual = sorted.First();
      Assert.AreEqual(expected, actual);

    }
    [TestMethod]
    public void Sort_DisplayZero_IsFirst()
    {
      var type = typeof(SortTester);

      var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
      var expected = props.First(p => p.Name == LinqOp.PropertyOf(() => new SortTester().ExpectedTrxCount).Name);
      var sorted = SortProperties(props);
      var actual = sorted.Skip(1).First();
      Assert.AreEqual(expected, actual);

    }
  }
}

Here's the source of LinqOp

Wednesday, November 10, 2010

Static reflection... or T4 with EnvDte?

So I like static reflection for getting Class, Property or Method names for databinding, but a lot of people aren't comfortable with the possible performance implications, or for whatever reason (usually FUD).  I've found an alternative using T4+EnvDte.

Code is included in the link...