Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can one make a C# attribute that is a combination of other attributes that get picked up by Entity Framework Code first migrations

I want to reduce the repeated code where I specify the same attributes for an object property used in multiple objects and associated database tables.

I am using property attributes to define how the property should be saved in the database and how it should be titled in UI elements. In my case this property appears in multiple tables/objects and I want it to have the same attributes everywhere. I also want these attributes to get picked up by Entity Framework's code first migrations. It looks like code first migrations loops over the attributes and looks for specific classes like MaxLengthAttribute or classes that inherit from the specific classes. Too bad that Entity Framework doesn't look for interfaces.

I don't want to move this string to a different table because the customers who will use these tables expect them to be directly queried by "CustomerNo".

for example:

[Table("foo")]
public class foo {
   …

   [Column(TypeName="varchar")]
   [MaxLength(15)]
   [Display(Name="Customer Identifier")]
   public string CustomerNo {get; set;}
   …
}

[Table("bar")]
public class bar {
   …

   [Column(TypeName="varchar")]
   [MaxLength(15)]
   [Display(Name="Customer Identifier")]
   public string CustomerNo {get; set;}
   …
}

What I would like to do is make a custom attribute that combines the above attributes into one like [CustomerNoAttribute] (I know I can leave off the suffix "Attribute" it is there to reduce confusion from the class CustomerNo).

There is no multiple inheritance so I cannot just inherit from ColumnAttribute, MaxLengthAttribute, and DisplayAttribute.

Is there a way I can use composition to make this work? e.g.

This code below doesn't work. The new internal attributes are not attached to the properties that I put [CustomerNoAttribute] on.

public CustomerNoAttribute: Attribute {

     public CustomerNoAttribute() {
          new MaxLengthAttribute(15);
          new DisplayAttribute().Name = "Customer Identifier";
          new ColumnAttribute().TypeName = "nvarchar";
     }
}

Is there another way, to reduce this repetition?

Techniques that use run time addition of attributes wont help because it looks like entity framework's code first migrations look at the compile time attributes only.

like image 529
Mike Wodarczyk Avatar asked May 21 '18 21:05

Mike Wodarczyk


1 Answers

The solution here is relatively simple, and one of my favourite features of the Entity Framework:

Code First Conventions

See Custom Code First Conventions for a full run through, the concept is that you can define your own arbitrary rules or conventions that the EF runtime should obey, this might be based on attributes but it doesn't have to be. You could create a convention based on the suffix or prefix on a field name if you really wanted.

Custom Conventions is a similar mechanism to Custom Type Descriptors as explained in this solution https://stackoverflow.com/a/38504552/1690217, except specifically for Entity Framework Code First models

You were on the right track, making custom attributes simplifies the implementation of custom code conventions, Display attribute however is problematic... Normally I would advise inheriting from the attribute that provides the most configuration, in this case DisplayAttribute, but we cannot inherit that type as it is sealed. I will unfortunately leave DisplayAttribute out of this solution as there are different convention concepts that can be employed at consumer end. This instead shows how you can use a custom attribute to replace multiple DataAnnotation based Attributes.

public CustomerNoAttribute : Attribute {
}

public class CustomerNoConvention : Convention
{
    public CustomerNoConvention()
    {
        this.Properties()
            .Where(p => p.GetCustomAttributes(false).OfType<CustomerNoAttribute>().Any()
            .Configure(c => c.HasColumnType("nvarchar")
                             .HasMaxLength(15)
                       );
    }
}

Now to use the custom attribute in your class:

[Table("foo")]
public class foo {
   …
    [CustomerNo]
    [Display(Name="Customer Identifier")]
    public string CustomerNo {get; set;}
   …
}

[Table("bar")]
public class bar {
   …
    [CustomerNo]
    [Display(Name="Customer Identifier")]
    public string CustomerNo {get; set;}
   …
}

Finally, we have to enable the custom convention, we can do this by overriding OnModelCreating in your DbContext class:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Conventions.Add(new CustomerNoConvention());
}

An alternate solution to reducing the duplicate entries of multiple attributes and conventions is of course to use inheritance:

public abstract class HasCustomerNo {
   …
    [CustomerNo]
    [Display(Name="Customer Identifier")]
    public string CustomerNo {get; set;}
   …
}
[Table("foo")]
public class foo : HasCustomerNo  {
   …
    // no need for CustomerNo 
   …
}

[Table("bar")]
public class bar : HasCustomerNo {
   …
    // no need for CustomerNo 
   …
}
like image 67
Chris Schaller Avatar answered Sep 28 '22 00:09

Chris Schaller