Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

February 2012 - Posts

General Purpose Data Annotations Validation Attribute

The Data Annotations framework, introduced in .NET 3.5, and enhanced in .NET 4.0, is likely becoming the basis for attribute-based validation in .NET. I like about it the fact that it is extensible and that it can be (and indeed it is) used in a lot of scenarios, from Silverlight to ASP.NET MVC and Dynamic Data.

Here’s an attribute I wrote for class and property validation. I wanted to be able to specify a C# validation expression, so that I could reuse the same attribute on different classes, just changing the expression, for example, “A > B”, “A != null”, and so. I chose C# as the expression language, but it can be easily made to support VB.

I used a less known feature of the DataTable class, calculated columns, that is, columns that can be produced from other columns or from plain expressions. Since calculated columns use a subset of SQL as its native language, I have to replace the C# expression for SQL, which is not that difficult, for this simple scenario.

Here’s the code:

   1: [Serializable]
   2: [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = true)]
   3: public sealed class ExpressionValidationAttribute : ValidationAttribute
   4: {        
   5:     public ExpressionValidationAttribute(String expression)
   6:     {
   7:         this.Expression = expression;
   8:     }
   9:  
  10:     /// <summary>
  11:     /// The expression to evaluate. May not be null.
  12:     /// Supported values:
  13:     /// PropertyName
  14:     /// null
  15:     /// {0}
  16:     /// Supported operators:
  17:     /// &gt;, &lt;, &gt;=, &lt;=, ==, !=
  18:     /// </summary>
  19:     /// <example>
  20:     /// PropertyA != null
  21:     /// PropertyA > PropertyB
  22:     /// </example>
  23:     public String Expression
  24:     {
  25:         get;
  26:         private set;
  27:     }
  28:  
  29:     public override Boolean IsDefaultAttribute()
  30:     {
  31:         return (this.Expression == null);
  32:     }
  33:  
  34:     public override Boolean Equals(Object obj)
  35:     {
  36:         if (base.Equals(obj) == false)
  37:         {
  38:             return (false);
  39:         }
  40:  
  41:         if (Object.ReferenceEquals(this, obj) == true)
  42:         {
  43:             return (true);
  44:         }
  45:  
  46:         ExpressionValidationAttribute other = obj as ExpressionValidationAttribute;
  47:  
  48:         if (other == null)
  49:         {
  50:             return (false);
  51:         }
  52:  
  53:         return (other.Expression == this.Expression);
  54:     }
  55:  
  56:     public override Int32 GetHashCode()
  57:     {
  58:         Int32 hashCode = 1;
  59:  
  60:         hashCode = (hashCode * 397) ^ (this.Expression != null ? this.Expression.GetHashCode() : 0);
  61:  
  62:         return (hashCode);
  63:     }
  64:  
  65:     public static String Replace(this String originalString, String oldValue, String newValue, StringComparison comparisonType)
  66:     {
  67:         Int32 startIndex = 0;
  68:  
  69:         while (true)
  70:         {
  71:             startIndex = originalString.IndexOf(oldValue, startIndex, comparisonType);
  72:             
  73:             if (startIndex < 0)
  74:             {
  75:                 break;
  76:             }
  77:  
  78:             originalString = String.Concat(originalString.Substring(0, startIndex), newValue, originalString.Substring(startIndex + oldValue.Length));
  79:  
  80:             startIndex += newValue.Length;
  81:         }
  82:  
  83:         return (originalString);
  84:     }
  85:  
  86:     protected override ValidationResult IsValid(Object value, ValidationContext validationContext)
  87:     {
  88:         if (String.IsNullOrWhiteSpace(this.Expression) == true)
  89:         {
  90:             return (ValidationResult.Success);
  91:         }
  92:  
  93:         Object instance = validationContext.ObjectInstance;
  94:         DataTable temp = new DataTable();
  95:         String expression = this.Expression;
  96:  
  97:         while (expression.IndexOf("  ") >= 0)
  98:         {
  99:             expression = expression.Replace("  ", " ");
 100:         }
 101:  
 102:         //translate .NET language operators into SQL ones
 103:         expression = expression.Replace("!=", "<>");
 104:         expression = expression.Replace("==", "=");
 105:         expression = expression.Replace("!", " NOT ");
 106:         expression = expression.Replace("&&", " AND ");
 107:         expression = expression.Replace("||", " OR ");
 108:         expression = Replace(expression, "= NULL", " IS NULL ", StringComparison.OrdinalIgnoreCase);
 109:         expression = Replace(expression, "<> NULL", " IS NOT NULL ", StringComparison.OrdinalIgnoreCase);
 110:         expression = Replace(expression, "null", "NULL", StringComparison.OrdinalIgnoreCase);
 111:         expression = expression.Replace("{0}", validationContext.MemberName);
 112:  
 113:         PropertyDescriptor[] props = TypeDescriptor
 114:             .GetProperties(instance)
 115:             .OfType<PropertyDescriptor>()
 116:             .Where(x => x.IsReadOnly == false)
 117:             .Where(x => x.PropertyType.IsPrimitive || x.PropertyType == typeof(String))
 118:             .ToArray();
 119:  
 120:         foreach (PropertyDescriptor prop in props)
 121:         {
 122:             temp.Columns.Add(prop.Name, prop.PropertyType);
 123:         }
 124:  
 125:         temp.BeginLoadData();
 126:  
 127:         DataRow row = temp.NewRow();
 128:  
 129:         temp.Rows.Add(row);
 130:  
 131:         foreach (PropertyDescriptor prop in props)
 132:         {
 133:             row[prop.Name] = prop.GetValue(instance);
 134:         }
 135:  
 136:         DataColumn isValidColumn = new DataColumn();
 137:         isValidColumn.ColumnName = "_is_valid";
 138:         isValidColumn.Expression = expression;
 139:  
 140:         temp.Columns.Add(isValidColumn);
 141:  
 142:         temp.EndLoadData();
 143:  
 144:         Boolean isValid = Convert.ToBoolean(row[isValidColumn]);
 145:  
 146:         if (isValid == true)
 147:         {
 148:             return (ValidationResult.Success);
 149:         }
 150:         else
 151:         {
 152:             String errorMessage = this.FormatErrorMessage(validationContext.MemberName != null ? validationContext.MemberName : validationContext.ObjectInstance.GetType().Name);
 153:             return (new ValidationResult(errorMessage, ((validationContext.MemberName != null) ? new String[] { validationContext.MemberName } : Enumerable.Empty<String>())));
 154:         }
 155:     }
 156: }

And a sample usage:

   1: [ExpressionValidation("A > B")]
   2: public class Test
   3: {
   4:     public Int32 A { get; set; }
   5:     public Int32 B { get; set; }
   6:     [ExpressionValidation("{0} != null")]
   7:     public String C { get; set; }
   8: }

As you can see, this attribute can be applied to either the whole class or a single property. If it is applied to a property, the {0} placeholder can be used instead of the property name; in any case, properties are referred by their names. All properties of primitive or string types that have both a getter and a setter can be used. No complex logic is allowed, such as calling String.Length for example. Maybe next time… Winking smile

Have fun!

Evento Introdução ao SharePoint 2010

No dia 25 de Fevereiro vai ter lugar em Leiria mais um evento organizado pelo Grupo de Utilizadores do SharePoint 2010 de Leiria, com o apoio da Galileu.

Fou convidado para falar sobre Business Connectivity Services. O nível é introdutório, se tiverem curiosidade, apareçam!

Email2012Fev

Changing a Control’s Output Dynamically

A question that pops up occasionally in ASP.NET forums or sites like Stack Overflow is how to change a control’s output dynamically. Well, since the CSS Friendly Control Adapters came out, I knew how to do it statically, through static registration on a .browser file (it can also be done dynamically, for all controls of a certain type, by adding a fully qualified type name to HttpContext.Current.Request.Browsers.Adapters collection), but I decided to go and try to do it dynamically, without registration. More specifically, I wanted to achieve two things:

  • Apply a XSL transformation to the generated output, provided it is XML-compliant;
  • Change the output through code in an event handler-like fashion.

So, here’s what I came up with:

   1: <%@ Register Assembly="WebApplication1" Namespace="WebApplication1" TagPrefix="web" %>
   2: ...
   3: <web:OutputAdapterControl runat="server" TargetControlID="table" OnOutput="OnOutput" XslPath="~/table.xsl" />
   4:  
   5: <asp:Table runat="server" ID="table"/>

I have two control declarations, one is for a well known asp:Table, and the other is for my OutputAdapterControl. On its declaration, I have a property that references the asp:Table through its id, one property for an external XSL transformation file and the final one for registering an event handler.

The code for the OutputAdapterControl is:

   1: namespace WebApplication1
   2: {
   3:     [NonVisualControl]
   4:     public class OutputAdapterControl : Control
   5:     {
   6:         private static readonly FieldInfo occasionalFieldsField = typeof(Control).GetField("_occasionalFields", BindingFlags.NonPublic | BindingFlags.Instance);
   7:         private static readonly FieldInfo flagsField = typeof(Control).GetField("flags", BindingFlags.NonPublic | BindingFlags.Instance);
   8:  
   9:         public OutputAdapterControl()
  10:         {
  11:             this.Enabled = true;
  12:         }
  13:  
  14:         public String XslPath
  15:         {
  16:             get;
  17:             set;
  18:         }
  19:  
  20:         public String TargetControlID
  21:         {
  22:             get;
  23:             set;
  24:         }
  25:  
  26:         public Boolean Enabled
  27:         {
  28:             get;
  29:             set;
  30:         }
  31:  
  32:         public event EventHandler<OutputEventArgs> Output;
  33:  
  34:         private ControlAdapter getControlAdapter(Control control)
  35:         {
  36:             Object flags = flagsField.GetValue(control);
  37:             MethodInfo setMethod = flags.GetType().GetMethod("Set", BindingFlags.NonPublic | BindingFlags.Instance);
  38:             setMethod.Invoke(flags, new Object[] { 0x8000 });
  39:  
  40:             Object occasionalFields = occasionalFieldsField.GetValue(control);
  41:             FieldInfo rareFieldsField = occasionalFields.GetType().GetField("RareFields");
  42:             Object rareFields = rareFieldsField.GetValue(occasionalFields);
  43:  
  44:             if (rareFields == null)
  45:             {
  46:                 rareFields = FormatterServices.GetUninitializedObject(rareFieldsField.FieldType);
  47:                 rareFieldsField.SetValue(occasionalFields, rareFields);
  48:             }
  49:  
  50:             FieldInfo adapterField = rareFields.GetType().GetField("Adapter");
  51:             ControlAdapter adapter = adapterField.GetValue(rareFields) as ControlAdapter;
  52:  
  53:             return (adapter);
  54:         }
  55:  
  56:         private void setControlAdapter(Control control, ControlAdapter controlAdapter)
  57:         {
  58:             Object occasionalFields = occasionalFieldsField.GetValue(control);
  59:             FieldInfo rareFieldsField = occasionalFields.GetType().GetField("RareFields");
  60:             Object rareFields = rareFieldsField.GetValue(occasionalFields);
  61:             FieldInfo adapterField = rareFields.GetType().GetField("Adapter");
  62:             adapterField.SetValue(rareFields, controlAdapter);
  63:         }
  64:  
  65:         internal void RaiseOutputEvent(OutputEventArgs e)
  66:         {
  67:             if (this.Output != null)
  68:             {
  69:                 this.Output(this, e);
  70:             }
  71:         }
  72:  
  73:         protected override void OnPreRender(EventArgs e)
  74:         {
  75:             if ((this.Enabled == true) && (String.IsNullOrWhiteSpace(this.TargetControlID) == false))
  76:             {
  77:                 Control control = this.FindControl(this.TargetControlID);
  78:                 ControlAdapter controlAdapter = this.getControlAdapter(control);
  79:                 OutputAdapterControlAdapter newAdapter = new OutputAdapterControlAdapter(this, controlAdapter, control, this.XslPath);
  80:  
  81:                 this.setControlAdapter(control, newAdapter);
  82:             }
  83:  
  84:             base.OnPreRender(e);
  85:         }
  86:     }
  87: }

And the code for the control adapter is:

   1: namespace WebApplication1
   2: {
   3:     public class OutputAdapterControlAdapter : ControlAdapter
   4:     {
   5:         private static readonly FieldInfo controlField = typeof(ControlAdapter).GetField("_control", BindingFlags.NonPublic | BindingFlags.Instance);
   6:         private static readonly MethodInfo controlRenderMethod = typeof(Control).GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance);
   7:         private static readonly MethodInfo controlAdapterRenderMethod = typeof(ControlAdapter).GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance);
   8:  
   9:         public OutputAdapterControlAdapter(OutputAdapterControl outputControl, ControlAdapter original, Control control, String xslPath)
  10:         {
  11:             this.OutputControl = outputControl;
  12:             this.Original = original;
  13:             this.XslPath = xslPath;
  14:             controlField.SetValue(this, control);
  15:         }
  16:  
  17:         protected OutputAdapterControl OutputControl
  18:         {
  19:             get;
  20:             private set;
  21:         }
  22:  
  23:         protected String XslPath
  24:         {
  25:             get;
  26:             private set;
  27:         }
  28:  
  29:         protected ControlAdapter Original
  30:         {
  31:             get;
  32:             private set;
  33:         }
  34:  
  35:         protected override void Render(HtmlTextWriter writer)
  36:         {
  37:             StringBuilder builder = new StringBuilder();
  38:             HtmlTextWriter tempWriter = new HtmlTextWriter(new StringWriter(builder));
  39:  
  40:             if (this.Original != null)
  41:             {
  42:                 controlAdapterRenderMethod.Invoke(this.Original, new Object[] { tempWriter });
  43:             }
  44:             else
  45:             {
  46:                 controlRenderMethod.Invoke(this.Control, new Object[] { tempWriter });
  47:             }
  48:  
  49:             if (String.IsNullOrWhiteSpace(this.XslPath) == false)
  50:             {
  51:                 String path = HttpContext.Current.Server.MapPath(this.XslPath);
  52:  
  53:                 XmlDocument xml = new XmlDocument();
  54:                 xml.LoadXml(builder.ToString());
  55:  
  56:                 builder.Clear();
  57:  
  58:                 XslCompiledTransform xsl = new XslCompiledTransform();
  59:                 xsl.Load(path);
  60:                 xsl.Transform(xml, null, tempWriter);
  61:             }
  62:  
  63:             OutputEventArgs e = new OutputEventArgs() { Html = builder.ToString() };
  64:  
  65:             this.OutputControl.RaiseOutputEvent(e);
  66:  
  67:             if (e.Html != builder.ToString())
  68:             {
  69:                 builder.Clear();
  70:                 builder.Append(e.Html);
  71:             }
  72:  
  73:             writer.Write(builder.ToString());
  74:         }
  75:     }
  76: }

Finally there’s the event argument:

   1: namespace WebApplication1
   2: {
   3:     [Serializable]
   4:     public class OutputEventArgs : EventArgs
   5:     {
   6:         public String Html
   7:         {
   8:             get;
   9:             set;
  10:         }
  11:     }
  12: }

And a sample event handler:

   1: protected void OnOutput(object sender, OutputEventArgs e)
   2: {
   3:     e.Html = e.Html.ToUpper();
   4: }

Please note that this relies heavily on reflection, and, for that matter, will only work with ASP.NET 4.0. It can be made to work with ASP.NET 2/3.5, but you will have to change the code to get to the stored control adapter in the Control class. Thanks to João Angelo for reminding me of that!

If you look at it carefully, you will see that, if an XSL transformation file is specified, the control adapter will use it to transform the output of the target control. Either way, if an event handler is registered, it will raise the event, passing in the event argument the HTML generated by the target control, possibly modified by the XSL transformation. In the event handler, all you have to do is change the Html property of the event argument.

Let me know if this helps in some way, and have fun!

NHibernate Metadata

I know that O/RMs like NHibernate are supposed to shield us from the gory database internals such as tables, views, columns and such; however, at times, I do have the need to know how my domain model translates to the database. NHibernate, of course, provides us with the information we need, but it isn’t always easy to find, so I wrote my own extension methods for ISessionFactory that allow me to get the metadata I need in an easy way:

   1: public static IEnumerable<PropertyInfo> GetComponents(this ISessionFactory sessionFactory, Type entityType)
   2: {
   3:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
   4:     IClassMetadata metadata = sessionFactory.GetClassMetadata(entityName);
   5:  
   6:     for (Int32 i = 0; i < metadata.PropertyTypes.Length; ++i)
   7:     {
   8:         IType property = metadata.PropertyTypes[i];
   9:  
  10:         if (property is ComponentType)
  11:         {
  12:             String propertyName = metadata.PropertyNames[i];
  13:             yield return (entityType.GetProperty(propertyName));
  14:         }
  15:     }
  16: }
  17:  
  18: public static IEnumerable<PropertyInfo> GetUserTypes(this ISessionFactory sessionFactory, Type entityType)
  19: {
  20:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
  21:     IClassMetadata metadata = sessionFactory.GetClassMetadata(entityName);
  22:  
  23:     for (Int32 i = 0; i < metadata.PropertyTypes.Length; ++i)
  24:     {
  25:         IType property = metadata.PropertyTypes[i];
  26:  
  27:         if (property is CustomType)
  28:         {
  29:             String propertyName = metadata.PropertyNames[i];
  30:             yield return (entityType.GetProperty(propertyName));
  31:         }
  32:     }
  33: }
  34:  
  35: public static IEnumerable<PropertyInfo> GetProperties(this ISessionFactory sessionFactory, Type entityType)
  36: {
  37:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
  38:     IClassMetadata metadata = sessionFactory.GetClassMetadata(entityName);
  39:     
  40:     for (Int32 i = 0; i < metadata.PropertyTypes.Length; ++i)
  41:     {
  42:         IType property = metadata.PropertyTypes[i];
  43:  
  44:         if (!(property is OneToOneType) && !(property is ManyToOneType) && !(property is ComponentType))
  45:         {
  46:             String propertyName = metadata.PropertyNames[i];
  47:             yield return (entityType.GetProperty(propertyName));                    
  48:         }
  49:     }
  50: }
  51:  
  52: public static IEnumerable<PropertyInfo> GetAssociations(this ISessionFactory sessionFactory, Type entityType)
  53: {
  54:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
  55:     IClassMetadata metadata = sessionFactory.GetClassMetadata(entityName);
  56:  
  57:     for (Int32 i = 0; i < metadata.PropertyTypes.Length; ++i)
  58:     {
  59:         IType property = metadata.PropertyTypes[i];
  60:  
  61:         if (property is OneToOneType)
  62:         {
  63:             String propertyName = metadata.PropertyNames[i];
  64:             yield return (entityType.GetProperty(propertyName));
  65:         }
  66:         else if (property is ManyToOneType)
  67:         {
  68:             String propertyName = metadata.PropertyNames[i];
  69:             yield return (entityType.GetProperty(propertyName));
  70:         }
  71:     }
  72: }
  73:  
  74: public static PropertyInfo GetIdentifier(this ISessionFactory sessionFactory, Type entityType)
  75: {
  76:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
  77:     AbstractEntityPersister persister = (sessionFactory as ISessionFactoryImplementor).TryGetEntityPersister(entityName) as AbstractEntityPersister;
  78:  
  79:     if (persister != null)
  80:     {
  81:         return (entityType.GetProperty(persister.IdentifierPropertyName));
  82:     }
  83:     else
  84:     {
  85:         return (null);
  86:     }
  87: }
  88:  
  89: public static IEnumerable<String> GetIdentifierColumnNames(this ISessionFactory sessionFactory, Type entityType)
  90: {
  91:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
  92:     AbstractEntityPersister persister = (sessionFactory as ISessionFactoryImplementor).TryGetEntityPersister(entityName) as AbstractEntityPersister;
  93:  
  94:     if (persister != null)
  95:     {
  96:         return (persister.IdentifierColumnNames);
  97:     }
  98:     else
  99:     {
 100:         return (null);
 101:     }
 102: }
 103:  
 104: public static String GetColumnName(this ISessionFactory sessionFactory, Type entityType, String propertyName)
 105: {
 106:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
 107:     AbstractEntityPersister persister = (sessionFactory as ISessionFactoryImplementor).TryGetEntityPersister(entityName) as AbstractEntityPersister;
 108:  
 109:     if (persister != null)
 110:     {
 111:         return (persister.GetPropertyColumnNames(propertyName).SingleOrDefault());
 112:     }
 113:     else
 114:     {
 115:         return (null);
 116:     }
 117: }
 118:  
 119: public static String GetTableName(this ISessionFactory sessionFactory, Type entityType)
 120: {
 121:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
 122:     AbstractEntityPersister persister = (sessionFactory as ISessionFactoryImplementor).TryGetEntityPersister(entityName) as AbstractEntityPersister;
 123:  
 124:     if (persister != null)
 125:     {
 126:         return (persister.TableName);
 127:     }
 128:     else
 129:     {
 130:         return (null);
 131:     }
 132: }
 133:  
 134: public static IEnumerable<PropertyInfo> GetCollections(this ISessionFactory sessionFactory, Type entityType)
 135: {
 136:     String entityName = (sessionFactory as ISessionFactoryImplementor).TryGetGuessEntityName(entityType);
 137:     IClassMetadata metadata = sessionFactory.GetClassMetadata(entityName);
 138:     String prefix = String.Concat(entityName, ".");
 139:     IDictionary<String, ICollectionMetadata> collectionMetadata = sessionFactory.GetAllCollectionMetadata().Where(x => x.Key.StartsWith(prefix)).ToDictionary(x => x.Key, x => x.Value);
 140:  
 141:     foreach (String key in collectionMetadata.Keys)
 142:     {
 143:         String propertyName = key.Replace(prefix, String.Empty);
 144:         yield return(entityType.GetProperty(propertyName));
 145:     }
 146: }

A brief explanation:

  • GetProperties: all the mapped properties for a given entity;
  • GetComponents: given an entity type, returns all the properties that are mapped as components;
  • GetUserTypes: all the properties that are mapped as custom user types;
  • GetIdentifier: the property that is mapped as the identifier;
  • GetAssociations: properties mapped as one to one or many to one, that is, that point to another entity;
  • GetCollections: properties mapped as one to many, that is, collections, of any type;
  • GetTableName: the table or view name where the entity is stored;
  • GetIdentifierColumnNames: the names of the column(s) that compose the primary key for the table;
  • GetColumnName: the table or view column that store a given property.
More Posts