Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC5 T4 ModelMetadata.Properties IsNullable or IsRequired

I am trying to create a replacement T4 template for the MVC5 Controller. I have everything working except for one issue.

I need to generate code for each property in the Model, and looping through ModelMetadata.Properties is actually really easy. However it is not an array of PropertyInfos. Rather it is an array of PropertyMetadata which doesn't seem to have any information about whether a property is required or not or if its type is nullable or not. So properties in your model of type int and int? both show up as type System.Int32.

Furthermore there is no way to get a list of PropertyInfos being that you can't really get a Type Object for the model which you are scaffolding, being that only the short type name is passed to the template.

In summation: Is there any way to know in a T4 template if a property is nullable?

like image 920
Rabbi Avatar asked Nov 01 '22 15:11

Rabbi


1 Answers

Problem

What needs to happen is to be able to crosswalk between the two entirely different classes

From: Microsoft.AspNet.Scaffolding.Core.Metadata.PropertyMetadata
To: System.Reflection.PropertyInfo

The PropertyMetadata class provided by ASP.NET Scaffolding exposes a relatively limited data, and no direct link to the original type or it's attributes.

Here's a comparison of the two properties : Property Comparison (click for full res)

because, honestly, why spend all this time working with a custom model generator without having access to the rich world of data provided by System.Reflection

Solution

  1. We're going to build a T4 helper method right inside of our normal web assembly. This is just any static method that can be consumed externally. We'll register it later inside of the T4 file so it can actually be used.

    So add a file like this anywhere in your project:

    UtilityTools.cs

    using System;
    using System.Reflection;
    using System.ComponentModel.DataAnnotations;
    namespace UtilityTools
    {
        public static class T4Helpers
        {
            public static bool IsRequired(string viewDataTypeName, string propertyName)
            {
                bool isRequired = false;
    
                Type typeModel = Type.GetType(viewDataTypeName);
                if (typeModel != null)
                {
                    PropertyInfo pi = typeModel.GetProperty(propertyName);
                    Attribute attr = pi.GetCustomAttribute<RequiredAttribute>();
                    isRequired = attr != null;
                }
    
                return isRequired;
            }
        }
    }
    
  2. Now let's register it by adding your assembly to the `Imports.include.t4: file:

    <#@ assembly name="C:\stash\cshn\App\MyProj\bin\MyProj.dll" #>
    

    Imports.include.t4

    Much in the same way that other assemblies are loaded, this actually gives us access to any static methods inside of whatever assembly we've loaded, minimally providing accessing to UtilityTools.T4Helpers.IsRequired

  3. Make sure you Rebuild your application (and possibly reload Visual Studio)

  4. Now we can make use of that inside any of our T4 templates like this:

    <#= UtilityTools.T4Helpers.IsRequired(ViewDataTypeName, property.PropertyName) #>
    

    IsRequired Helper usage

Further Reading:

The above solution is heavily adapted from the following articles & questions:

  • MVC 5 T4 Templates and View Model Property Attributes
  • MVC 4 custom data annotations read in T4 scaffolding
  • Get scaffolder to generate fields in specific order

Tip: Debugging T4 templates can sometimes be a bear, so I wrote this test template which helps dump out information from the model so you can easily evaluate ASP.NET is using as values to hydrate PropertyMetadata Gist: https://gist.github.com/KyleMit/fc9ccfbc2af03462d660257103326509


Note: Type.GetType("Namespace.Qualified.TypeName") only works when the type is found in either mscorlib.dll or the currently executing assembly. If your model is part of a business library, you either have to use a AssemblyQualifiedName like this:

Type.GetType("Namespace.Qualified.TypeName,DllName")

or you can search through all of the currently executing assemblies like this:

public static Type GetType(string typeName)
{
    var type = Type.GetType(typeName);
    if (type != null) return type;
    foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
    {
        type = a.GetType(typeName);
        if (type != null)
            return type;
    }
    return null;
}

in order for this to work, you'll have to add any business libraries that contain classes to the list of imported assemblies (same as we did earlier in the imports.t4 file):

<#@ assembly name="C:\stash\cshn\App\MyProj\bin\MyProj.Business.dll" #>
like image 66
KyleMit Avatar answered Nov 08 '22 05:11

KyleMit