MaxLength w EntityFramework oraz Text Template Transformation Toolkit (T4).

Podczas generowania modelu encji na podstawie bazy danych, EF potrafi pobrać maksymalną długość pola. Jeśli kolumna w bazie posiada typ nvarchar(50) to EF zmapuje to na zmienną typ string oraz ustawi pole MaxLength na wartość 50.

image

Informacja o dozwolonej długości (50) jest zapisana w metadanych. Niestety próba przypisania wartości dłuższej niż 50 znaków zakończy się powodzeniem ponieważ nvarchar(50) został zmapowany na string, który nie ma ograniczenia do 50 znaków. Poniższy kod NIESTETY zadziała:

Contact contact = new Contact();
contact.Email="jakis napis dluzszy niz 50 znakow..."

W jaki sposób walidować więc  dane? Gdy przekażemy napis dłuższy niż 50 znaków wygenerowana encja nie wyrzuci wyjątku jednak podczas zapisu informacji danych do bazy danych oczywiście wyjątek zostanie zwrócony:

String or binary data would be truncated.

Należy zatem w jakiś sposób walidować dane podczas próby przypisania ich do konkretnych właściwości. Jednym ze sposobów jest wykorzystanie klas częściowych. Można w końcu stworzyć klasę, która będzie walidowała dane np:

public partial class Contact
{
   public IEnumerable<RuleViolation> GetRuleViolations()
   {
       if (Email.Length>50)
           yield return new RuleViolation("Nieprawidłowa długość pola email.", "Email");
   }
   public void Validate()
   {
       if (GetRuleViolations().Count() != 0)
           throw new Validation.ValidationException("Encja zawiera nieprawdiłowe dane");
   }
}

gdzie RuleViolation to:

public class RuleViolation
{
   public string ErrorMessage { get; private set; }
   public string PropertyName { get; private set; }

   public RuleViolation(string errorMessage, string propertyName)
   {
       ErrorMessage = errorMessage;
       PropertyName = propertyName;
   }
}

Rozwiązanie ma zasadniczą wadę – każde pole trzeba ręcznie sprawdzać. Jak już wspomniałem EF posiada w metadanych MaxLength więc pomyślny jak można wykorzystać już zapisane dane.

Rozwiązaniem jest tzw. Text Template Transformation Toolkit (T4). T4 jest silnikiem automatycznego generowania kodu. Za pomocą szablonów T4 możemy generować klasy, metody itp. Szablony tworzone są za pomocą specjalnego języka dyrektyw przypominającego nieco dyrektywy ASP .NET. Wykonanie T4 składa się z następujących etapów:

image

Po skompilowaniu szablonu zostanie stworzona klasa GeneratedTextTransformation (wraz z przeładowaną metodą TransformText) odpowiedzialna za wygenerowanie kodu opisanego przez Text Template. Nie będę w tym poście opisywał dokładnie składni T4 ponieważ jest to zdecydowanie temat wymagający przynajmniej kilku postów. Przedstawię tylko przykładowy szablon:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<#Write("class HelloClass\n{");#>   
    public void HelloWorld()
    {
            
    }
}

Kod wygenerowany na podstawie powyższego szablonu:

class HelloClass
{   
    public void HelloWorld()
    {            
    }
}

Wróćmy do głównego tematu. Klasy encji EF generowane są właśnie na podstawie T4. Aby zobaczyć kod T4 wystarczy kliknąć prawym guzikiem myszki na modelu encji (np. plik Entities.edmx) i wybrać Add Code Generation Item. Pojawi się okno w którym zaznaczamy ADO .NET EntityObject Generator.

image

Powstanie plik o rozszerzeniu .tt. Nie będę tutaj wklejał jego zawartości ponieważ jest zdecydowanie zbyt długa :). Szablon generuje klasy wszystkich encji. Jeśli szablon został nazwany Model1.tt to Model1.cs będzie zawierał wygenerowane klasy encji w C#.

Modyfikując szablon możemy więc wpływać na zachowanie generowanych encji. Nie ma więc problemu abyśmy dopisali logikę walidacyjną odpowiedzialną za sprawdzanie długości string’a. W tej chwili szablon generuje właściwości w następujący sposób:

[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
[DataMemberAttribute()]
public global::System.String Email
{
  get
  {
      return _Email;
  }
  set
  {        
      OnEmailChanging(value);

      ReportPropertyChanging("Email");
      _Email = StructuralObject.SetValidValue(value, true);
      ReportPropertyChanged("Email");
      OnEmailChanged();
  }
}

Z kolei my chcemy aby przypisanie właściwości wyglądało:

   [EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=true)]
        [DataMemberAttribute()]
        public global::System.String Email
        {
            get
            {
                return _Email;
            }
            set
            {                                
                if (value!=null&& value.Length > 50) 
                { 
                   throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.",PoliticiansPromises.Dictionaries.PropertyMappingsManager.Instance["Email"], "50"),Email);
                }
    
                OnEmailChanging(value);
    
                ReportPropertyChanging("Email");
                _Email = StructuralObject.SetValidValue(value, true);
                ReportPropertyChanged("Email");
                OnEmailChanged();
            }
        }

 

Można to zrealizować w łatwy sposób dopisując odpowiedni kod do szablonu T4. Największą chyba trudnością jest znalezienie w tym długim szablonie fragmentu odpowiedzialnego za generowanie setter’a. W każdym razie fragment T4 odpowiedzialny za wygenerowanie logiki walidacyjnej wygląda następująco:

<#+       
if (code.Escape(primitiveProperty.TypeUsage) == "global::System.String")
{           
    string facetName = "MaxLength";
   int maxLength = 0;
   if (Int32.TryParse(primitiveProperty.TypeUsage.Facets[facetName].Value.ToString(), out maxLength))
   {

#>            
        if (value!=null&& value.Length > <#= maxLength.ToString() #>) 
       { 
          throw new PoliticiansPromises.Entities.Validation.ValidationPropertyException(String.Format("Pole {0} może mieć maksymalnie {1} znaków.","<#= code.Escape(primitiveProperty) #>", "<#= maxLength.ToString() #>"),<#=code.Escape(primitiveProperty)#>);
       }
<#+
  }
}                    
                
#>

 

Najpierw sprawdzamy czy właściwość jest typu String a następnie pobieramy z metadanych maksymalną długość. Po dopisaniu powyższego fragmentu w odpowiednim miejscu wszystkie encje zostaną wygenerowane ze stosowną logika walidacyjną. Kod powinien  być dopisany bezpośrednio po następującym fragmencie T4:

 

    <#=code.SpaceAfter(Accessibility.ForSetter((primitiveProperty)))#>set
        {        
<#+
        if (ef.IsKey(primitiveProperty))
            {
                if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[]))
                {
#>
            if (!StructuralObject.BinaryEquals(<#=code.FieldName(primitiveProperty)#>, value))
<#+
                }
                else
                {
#>
            if (<#=code.FieldName(primitiveProperty)#> != value)
<#+
                }
#>
            {
<#+
        PushIndent(CodeRegion.GetIndent(1));
            }
#>

 

Od teraz programista po dodaniu nowej encji nie musi pisać żadnego kodu odpowiedzialnego za walidacje długości samodzielnie.

2 thoughts on “MaxLength w EntityFramework oraz Text Template Transformation Toolkit (T4).”

  1. A co w przypadku zmiany schematu bazy (dodanie/modyfikacja tabeli)?? Nasze zmiany w pliku .tt zostaną zachowane, czy też może usunięte/nadpisane?

  2. Plik tt nie zmienia sie – on wykorzystuje zewnętrzny plik XML(model encji). Dodając tabele modyfikujesz plik XMl a nie szablon tt.

Leave a Reply

Your email address will not be published.