Zur Zeit wird gefiltert nach: dbcontext
Filter zurücksetzen
Entity Framework - Ein neuer Workaround für 2nd Level Cache mit dem DbContext
Bisher habe ich den 2nd Level Cache nur über einen Umweg über den ObjectContext zum laufen gebracht. Pawel Kadluczka vom EF-Team hat Ende März einen weiteren Ansatz in seinen Blog veröffentlicht, der ohne den ObjectContext auskommt.
Dieser kann auch in Verbindung mit SchemaMigrations zusammen arbeiten, wenn die Tricks und Kniffe im Beitrag beachtet werden.
Wenn das EF-Team an solchen Workarounds rumbastelt, kann davon ausgegangen werden, dass der 2nd Level Cache weiterhin kein Bestandteil des Kerns von EF werden wird. Zum Beitrag geht es hier lang.
Weitere Informationen zum Thema
Entity Framework - DbContext und der 2nd-Level Cache mit dem EFCachingProvider
Der DbContext stellt eigentlich ein Wrapper auf den ObjectContext dar. Ich bevorzuge mittlerweile den DbContext, da dieser einige Funktionalitäten bietet, die ich beim ObjectContext vermisse. Da wären bspw. das vereinfachte Change Tracking, der Support der DataAnnotations, der veinfachte Zugriff auf dem Context-Cache über die Local-Eigenschaft und das dieser neben Code First auch mit dem EDM-Designer verwendet werden kann.
Ein Nachteil ist jedoch die fehlende Unterstützung für das Caching. Für den ObjectContext gibt es den EFCachingProvider, der seine Aufgaben relativ gut verrichtet. Über den Konstruktor im ObjectContext wird dieser aktiviert.
Nun ist der DbContext ja nichts anderes als ein Wrapper und diesen kann auch eine ObjectContext-Instanz übergeben werden. Also kam ich auf eine ganz „wilde“ Idee, um den DbContext mit einer Zusammenarbeit mit dem EFCachingProvider zu überreden. Alternativ liesse sich das auch mit dem Proxy-Pattern realisieren, aber ich gebe die Hoffnung nicht auf, dass der EFCachingProvider ein fester Bestandteil vom EF werden wird.
Mit Hilfe der mitgelieferten T4-Vorlage, die den Einsatz des DbContext für DbFirst und ModelFirst ermöglicht, lässt sich der Caching-Support sehr einfach realisieren. Die angepasste T4-Vorlage sieht so aus:
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
var loader = new MetadataLoader(this);
var region = new CodeRegion(this);
var inputFile = @"SimpleModel.edmx";
var ItemCollection = loader.CreateEdmItemCollection(inputFile);
Code = new CodeGenerationTools(this);
EFTools = new MetadataTools(this);
ObjectNamespace = Code.VsNamespaceSuggestion();
ModelNamespace = loader.GetModelNamespace(inputFile);
EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------
<#
if (!String.IsNullOrEmpty(ObjectNamespace))
{
#>
namespace <#=Code.EscapeNamespace(ObjectNamespace)#>
{
<#
PushIndent(CodeRegion.GetIndent(1));
}
#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.IO;
using EFProviderWrapperToolkit;
using EFTracingProvider;
using EFCachingProvider;
using EFCachingProvider.Caching;
// Workaround, create an objectcontext for 2nd Level Cache with EFProviderToolkit
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#>Context : ObjectContext
{
private TextWriter logOutput;
#region Konstruktoren
public <#=Code.Escape(container)#>Context()
: base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
"name=<#=container.Name#>",
/*"EFTracingProvider",*/
"EFCachingProvider"
), "<#=container.Name#>")
{
}
public <#=Code.Escape(container)#>Context(string connectionString)
: base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
connectionString,
/*"EFTracingProvider",*/
"EFCachingProvider"
))
{
}
#endregion
// ObjectSets are not required, when we use the DbContext.
#region Tracing Extensions
private EFTracingConnection TracingConnection
{
get { return this.UnwrapConnection<EFTracingConnection>(); }
}
public event EventHandler<CommandExecutionEventArgs> CommandExecuting
{
add { this.TracingConnection.CommandExecuting += value; }
remove { this.TracingConnection.CommandExecuting -= value; }
}
public event EventHandler<CommandExecutionEventArgs> CommandFinished
{
add { this.TracingConnection.CommandFinished += value; }
remove { this.TracingConnection.CommandFinished -= value; }
}
public event EventHandler<CommandExecutionEventArgs> CommandFailed
{
add { this.TracingConnection.CommandFailed += value; }
remove { this.TracingConnection.CommandFailed -= value; }
}
private void AppendToLog(object sender, CommandExecutionEventArgs e)
{
if (this.logOutput != null)
{
this.logOutput.WriteLine(e.ToTraceString().TrimEnd());
this.logOutput.WriteLine();
}
}
public TextWriter Log
{
get { return this.logOutput; }
set
{
if ((this.logOutput != null) != (value != null))
{
if (value == null)
{
CommandExecuting -= AppendToLog;
}
else
{
CommandExecuting += AppendToLog;
}
}
this.logOutput = value;
}
}
#endregion
#region Caching Extensions
private EFCachingConnection CachingConnection
{
get { return this.UnwrapConnection<EFCachingConnection>(); }
}
public ICache Cache
{
get { return CachingConnection.Cache; }
set { CachingConnection.Cache = value; }
}
public CachingPolicy CachingPolicy
{
get { return CachingConnection.CachingPolicy; }
set { CachingConnection.CachingPolicy = value; }
}
#endregion
}
// Workaround ends here
// DbContext
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#> : DbContext
{
// Modification: Constructor with ObjectContext init
public <#=Code.Escape(container)#>()
: base(new <#=Code.Escape(container)#>Context(), true)
{
<#
WriteLazyLoadingEnabled(container);
#>
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
<#
foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
{
#>
<#=Accessibility.ForReadOnlyProperty(entitySet)#> DbSet<<#=Code.Escape(entitySet.ElementType)#>> <#=Code.Escape(entitySet)#> { get; set; }
<#
}
foreach (var edmFunction in container.FunctionImports)
{
WriteFunctionImport(edmFunction, false);
}
#>
}
<#
if (!String.IsNullOrEmpty(ObjectNamespace))
{
PopIndent();
#>
}
<#
}
#>
<#+
string ModelNamespace { get; set; }
string ObjectNamespace { get; set; }
CodeGenerationTools Code { get; set; }
MetadataTools EFTools { get; set; }
string GetResourceString(string resourceName)
{
if(_resourceManager == null)
{
_resourceManager = new System.Resources.ResourceManager("System.Data.Entity.Design", typeof(System.Data.Entity.Design.MetadataItemCollectionFactory).Assembly);
}
return _resourceManager.GetString(resourceName, null);
}
System.Resources.ResourceManager _resourceManager;
void WriteLazyLoadingEnabled(EntityContainer container)
{
string lazyLoadingAttributeValue = null;
var lazyLoadingAttributeName = MetadataConstants.EDM_ANNOTATION_09_02 + ":LazyLoadingEnabled";
if(MetadataTools.TryGetStringMetadataPropertySetting(container, lazyLoadingAttributeName, out lazyLoadingAttributeValue))
{
bool isLazyLoading;
if(bool.TryParse(lazyLoadingAttributeValue, out isLazyLoading) && !isLazyLoading)
{
#>
this.Configuration.LazyLoadingEnabled = false;
<#+
}
}
}
void WriteFunctionImport(EdmFunction edmFunction, bool includeMergeOption)
{
var parameters = FunctionImportParameter.Create(edmFunction.Parameters, Code, EFTools);
var paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
var returnType = edmFunction.ReturnParameter == null ? null : EFTools.GetElementType(edmFunction.ReturnParameter.TypeUsage);
var processedReturn = returnType == null ? "int" : "ObjectResult<" + MultiSchemaEscape(returnType) + ">";
if (includeMergeOption)
{
paramList = Code.StringAfter(paramList, ", ") + "MergeOption mergeOption";
}
#>
<#=AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction))#> <#=processedReturn#> <#=Code.Escape(edmFunction)#>(<#=paramList#>)
{
<#+
if(returnType != null && (returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType ||
returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType))
{
#>
((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace.LoadFromAssembly(typeof(<#=MultiSchemaEscape(returnType)#>).Assembly);
<#+
}
foreach (var parameter in parameters.Where(p => p.NeedsLocalVariable))
{
var isNotNull = parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null";
var notNullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", " + parameter.FunctionParameterName + ")";
var nullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", typeof(" + parameter.RawClrTypeName + "))";
#>
var <#=parameter.LocalVariableName#> = <#=isNotNull#> ?
<#=notNullInit#> :
<#=nullInit#>;
<#+
}
var genericArg = returnType == null ? "" : "<" + MultiSchemaEscape(returnType) + ">";
var callParams = Code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()));
if (includeMergeOption)
{
callParams = ", mergeOption" + callParams;
}
#>
return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<#=genericArg#>("<#=edmFunction.Name#>"<#=callParams#>);
}
<#+
if(!includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)
{
WriteFunctionImport(edmFunction, true);
}
}
string AccessibilityAndVirtual(string accessibility)
{
return accessibility + (accessibility != "private" ? " virtual" : "");
}
string MultiSchemaEscape(TypeUsage usage)
{
var type = usage.EdmType as StructuralType;
return type != null && type.NamespaceName != ModelNamespace ?
Code.CreateFullName(Code.EscapeNamespace(type.NamespaceName), Code.Escape(type)) :
Code.Escape(usage);
}
#>
Für den CodeFirst-Ansatz wird das Ganze schon ein wenig wilder, da das Model zur Laufzeit erstellt wird und der Metadaten-ConnectionString in dieser Form auch nicht existiert. Aber auch hier habe ich mit ein wenig „Basteln“ eine lauffähige Variante hinbekommen. Da dieser Workaround ziemlich wild ist, habe ich diesen als T4-Vorlage realisiert. Folgende Vorteile ergeben sich daraus: Der Workaround ist dokumentiert und lässt sich leichter entfernen, wenn in einer zukünftigen Version der 2nd-Level Cache endlich mal realisiert wird.
Die T4-Vorlage für den wildesten aller Workarounds ist hier:
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="Microsoft.CSharp" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="$(ProjectDir)$(OutDir)EntityFramework.dll" #>
<#@ assembly name="$(TargetPath)" #> // in this case dll with codefirst context
<#@ import namespace="System.Data.EntityClient" #>
<#@ import namespace="System.Data.Entity.Infrastructure" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ include file="TemplateFileManager.CS.ttinclude" #>
<#@ include file="VsAutomationHelper.CS.ttinclude" #>
<#@ include file="ConfigurationAccessor.CS.ttinclude" #>
<#
// ToDo: Initalize an instance of the DbContext (T4 uses connection by convention)
var contextInstance = new DbContextCodeFirst2ndLevelCache.TestContext();
// Change database connection
// Workaround for accessing the app.config from t4 [random appdomain :-(]
ConfigurationAccessor config = new ConfigurationAccessor((IServiceProvider)this.Host);
// set the connectionstring
string cn = config.ConnectionStrings.Cast<ConnectionStringSettings>()
.Where(c=>c.Name == contextInstance.GetType().Name).Single().ConnectionString;
contextInstance.Database.Connection.ConnectionString = cn;
string contextName = contextInstance.GetType().Name;
string contextNamespace = contextInstance.GetType().Namespace;
string contextSuffix = "OC";
var fileManager = TemplateFileManager.Create(this);
// File properties for C# output files
var compileProp = new FileProperties();
compileProp.BuildAction = BuildAction.Compile;
// File properties for EDMX output
var edmxProp = new FileProperties();
edmxProp.BuildAction = BuildAction.EntityDeploy;
edmxProp.CustomTool = "EntityModelCodeGenerator";
// Create Edmx file
fileManager.StartNewFile(String.Format("{0}.edmx", contextName)
, fileProperties:edmxProp);
this.Write(this.GetEdmxFileFromDbContext(contextInstance));
// Create the ObjectContext with ef provider wrapper
fileManager.StartNewFile(String.Format("{0}{1}.cs", contextName, contextSuffix)
, fileProperties:compileProp);
CreateObjectContextFromDbContext(contextName, contextNamespace, contextSuffix);
fileManager.Process(true);
#>
<#+
void CreateObjectContextFromDbContext(string contextName, string contextNamespace, string contextSuffix)
{
#>
namespace <#= contextNamespace #>
{
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.Entity;
using System.Data.EntityClient;
using System.Data.Objects;
using EFProviderWrapperToolkit;
using EFTracingProvider;
using EFCachingProvider;
using EFCachingProvider.Caching;
public class <#= contextName #><#= contextSuffix #> : ObjectContext
{
public <#= contextName #><#= contextSuffix #>()
: base (EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
TransformToMetaDataConnectionString("<#= contextName #>"),
/*"EFTracingProvider",*/
"EFCachingProvider"
), "<#= contextName #>")
{
}
public static string TransformToMetaDataConnectionString(string contextName)
{
string folderres = "Workaround."; //Subfolder of workaround with edmx
var con = System.Configuration.ConfigurationManager.ConnectionStrings
.Cast<ConnectionStringSettings>()
.Where(c => c.Name == contextName)
.Single();
if (con == null)
{
throw new ArgumentException("No connection with name '{0}' found.", contextName);
}
var conn = new EntityConnectionStringBuilder();
conn.ProviderConnectionString = con.ConnectionString;
conn.Provider = con.ProviderName;
conn.Metadata = String.Format("res://*/{1}{0}.csdl|res://*/{1}{0}.ssdl|res://*/{1}{0}.msl", contextName, folderres);
return conn.ConnectionString;
}
}
}
<#+
}
/// <summary>
/// The code first context must already exists and compiled. Be sure that
/// the connection string exists in the app.config and the database is created.
/// Required for edmx file creation
/// </summary
string GetEdmxFileFromDbContext(System.Data.Entity.DbContext ctx)
{
string xml = String.Empty;
var sw = new UTF8StringWriter();
using (var writer = new XmlTextWriter(sw))
{
EdmxWriter.WriteEdmx(ctx, writer);
}
xml = sw.ToString();
return xml;
}
public class UTF8StringWriter : StringWriter
{
public override Encoding Encoding
{
get { return Encoding.UTF8; }
}
}
#>
Die Beispiel-Solution kann hier heruntergeladen werden. Für die DbFirst bzw. ModelFirst-Variante muss zuerst eine Datenbank mit den Namen DbContextTest angelegt werden, dass SQL-Skript befindet sich im Verzeichnis SQL-Skripts. Der EFCachingProvider in der Solution ist modifiziert, die Anpassungen sind auch in diesem Blog beschrieben.
Entity Framework 4.1 - Readonly Properties in der Datenbank speichern
Über das NotMapped-Attribut habe ich schon einen Beitrag geschrieben und es als Beispiel einer Getter-Eigenschaft in Form einer Anschrift verwendet.
Bei Berechnungen kann das Ganze sehr hilfreich sein aber was passiert, wenn das Reporting direkt auf die Datenbank zugreift? Die Readonly-Eigenschaften sind in diesem Fall nicht als Spalten in der Tabelle vorhanden.
Um dieses Problem zu lösen, gibt es mehrere Szenarien.
Ein Lösungsansatz wäre bspw. eine berechnete Spalte (Computed Column) anzulegen und die Formel auch im Datenmodell zu hinterlegen. Hier entsteht jedoch eine Redundanz. Bei einer Anpassung der Formel muss das künftig an zwei Orten gemacht werden: Im Business Layer und auf der Datenbank. Nicht das, was in diesem Szenario erstrebenswert ist. Wenn der Kunde die Unterstützung für verschiedene Datenbanken wünscht, wird es zudem noch richtig spannend.
Ein weiterer Lösungsansatz wäre ein Workaround innerhalb der Readonly-Eigenschaft. Durch das Setzen eines leeren Setter wird der berechnete Wert auch in der Datenbank gespeichert. Der Vorteil, die Datenbank ist nicht für einzelne Berechnungsschritte verantwortlich, die im schlimmsten Fall die Erstellung des Reports verzögert.
Schauen wir das Beispiel zum letzten Lösungsansatz an:
public class Order
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public double Price { get; set; }
[Required(ErrorMessage = "Das Eingabefeld ist notwendig.")]
public int Quantity { get; set; }
[Required(ErrorMessage = "Das Eingabefeld ist notwendig.")]
public double Tax { get; set; }
public double Total
{
get
{
return (this.Price * this.Quantity) * (100 + Tax) / 100;
}
set { } // Workaround ReadOnly in Datenbank speichern
}
}
Der leere Setter ist der Workaround zur Realisierung dieser Anforderung. Ein kleiner Nachteil, während der Entwicklung können Werte auf diese Eigenschaft zugewiesen werden, ohne das Visual Studio bzw. der Compiler eine Warnung ausgibt, dass es sich um eine Readonly-Eigenschaft handelt.
Und unter den NHibernate-Fans, ja mir ist bewusst, dass es mit NHibernate eine bessere Lösung gibt.
Machen wir dazu einen kleinen Test und prüfen, ob die Formel richtig rechnet. Dies könnte wie in folgender Abbildung realisiert werden:
Kommt jetzt die Anforderung hinzu, dass für die Reports der Anwendung ein firmenweites Standard-Tool wie zum Beispiel Cognos verwendet werden muss, erspare ich mir so die Formelredundanz.
Aber es muss natürlich auch darauf geachtet werden, dass bei Änderungen an der Formel, die Werte in der Datenbank ebenfalls für jeden Datensatz neu kalkuliert werden müssen, da sonst die Reports mit falschen Zahlenmaterial arbeiten.
Entity Framework 4.1 Mapping Szenarien
Im Rahmen einer Präsentation mit Zeitvorgabe ;-) entstand ein kleines Beipspielprojekt mit den geläufigsten Mapping-Szenarien beim Code First - Ansatz.
Der Beispielcode steht auf der Seite der .NET User Group Bern als Zip zum Download bereit.
Entity Framework 4.1 und die DataAnnotations bei DB First
Mit der Stand alone - Installation von EF 4.1 steht auch eine einfache T4-Vorlage zur Verfügung, die den DbContext für die Verwendung mit dem Entity Designer ermöglicht. Ein hilfreiches Feature ist die Validierung mit den DataAnnotations, welche beim Code First-Ansatz auch Einfluss auf das Datenbankdesign haben.
Mit der T4-Vorlage werden jedoch nur die Klassen erstellt. Da die Datenbank besteht, werden die Pflichtfelder und die Eingabelänge bereits definiert sein.
Folgende Abbildungen verdeutlichen dies:
Es werden einfache POCO-Klassen erstellt. Wer die DataAnnonations auch erstellt haben will, der muss das Selbst tun. Mit T4 geht das auch einfach von der Hand. Der Vorteil dieser Variante ist ganz klar die Individualität, die dadurch erreicht werden kann. Gerade bei mehrsprachigen Anwendungen ist die Verwendung von Ressourcen sinnvoll. In manchen Fällen wurden prozessspezifische Anpassungen vorgenommen, sodass die neue T4-Vorlage auf diese Prozesse abgestimmt werden kann.
Meine Vorgehensweise ist sehr einfach. Ich habe eine Quick & Dirty - Vorlage erstellt, die aus den Angaben des Entity Designers die Infrastruktur für die Validierung erstellt. In ca. 10 min war ich damit auch fertig.
Als erstes habe ich den Inhalt aus der T4-Vorlage verwendet, die für die Erstellung der POCO's verantwortlich ist. Anschliessend habe ich den EntityFrameworkTemplateFileManager durch den TemplateFileManager ersetzt und die Klassenerstellung gemäss den DataAnnotations angepasst.
Die T4-Vorlage hat nun folgenden Aufbau:
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="Microsoft.CSharp" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ include file="TemplateFileManager.CS.ttinclude" #>
<#
var fileManager = TemplateFileManager.Create(this);
CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this, 1);
MetadataTools ef = new MetadataTools(this);
string inputFile = @"Model1.edmx";
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
string namespaceName = code.VsNamespaceSuggestion();
foreach (var entity in ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
fileManager.StartNewFile(entity.Name + ".DataAnnotations.cs");
this.CreateDataAnnotationClass(code, entity, namespaceName);
}
foreach (var entity in ItemCollection.GetItems<ComplexType>().OrderBy(e => e.Name))
{
fileManager.StartNewFile(entity.Name + ".DataAnnotations.cs");
this.CreateDataAnnotationClass(code, entity, namespaceName);
}
fileManager.Process();
#>
<#+
void CreateDataAnnotationClass(CodeGenerationTools code, object entityType, string namespaceName)
{
dynamic entity = entityType as EntityType;
if (entity == null)
entity = entityType as ComplexType;
#>
namespace <#= namespaceName #>
{
using System.Data;
using System.ComponentModel.DataAnnotations;
[MetadataType(typeof(<#= entity.Name #>MetaData))]
public partial class <#= entity.Name #>
{
}
public class <#= entity.Name #>MetaData
{
<#+
foreach (var item in entity.Properties)
{
#>
<#= item.Nullable == false ? String.Empty : "[Required(ErrorMessage=\"Message Required\")]" #>
<#= this.TransformStringLenghtAttribute(item) #>
public object <#= item.Name #> {<#= code.SpaceAfter(Accessibility.ForGetter(item)) #> get;<#= code.SpaceAfter(Accessibility.ForSetter(item)) #> set; }
<#+
}
#>
}
}
<#+
}
public string TransformStringLenghtAttribute(EdmProperty item)
{
Facet f = item.TypeUsage.Facets.Where(t => t.Name == "MaxLength").FirstOrDefault();
string sl = String.Empty;
if (f != null)
{
string length = f.Value.ToString();
if (length == "Max")
length = Int32.MaxValue.ToString();
sl = String.Format("[StringLength({0}, ErrorMessage=\"Too long\")]", length);
}
return sl;
}
#>
Mit ein bisschen mehr Zeit kann die Vorlage auch besser umgesetzt und erweitert werden. So können zum Beispiel auch Validierungsmechanismen aus Datentypen oder Namen (Email) abgeleitet werden.
Die Klassen, die nun erstellt werden, haben folgenden Aufbau:
Der Vorteil dieser Variante ist auch, dass eine Kombination mit anderen Validierungsschnittstellen möglich wird. So liesse sich bspw. auch der Einsatz von Reflection reduzieren, aber das ist bekanntlich Geschmacksache.





Social Bookmarking