A library for .NET that uses source generators to automatically generate data annotations for your models. It provides a strongly-typed mechanism to define your annotation rules.
Data annotations are attributes that are applied to the class or members that specify validation rules and how the data is displayed. They've been around for a long time, and there is great support across various frameworks in the .NET ecosystem. There is built-in support in Winform/WPF controls (e.g., grid controls, edit forms), ASP.NET MVC/Razor Pages, EntityFramework, Blazor (support for validation attributes was added recently), and many other frameworks. If you're applying localization in your applications, annotations are a great mechanism to localize the labels for your properties and define how the data will be formatted and displayed (e.g., date formats, numeric formats). Keeping these concerns bundled with your models might simplify the usage significantly and you'll avoid duplication throughout your code.
Having said that, I'm a fan of annotations and tend to use them wherever possible. But, the "huge" downside is that your models become cluttered with these extra attributes. For example, let's take this very simple model.
public class Login
{
[Display(Order = 1, Name = "Username", Prompt = "Enter your username", ResourceType = typeof(AppResources))]
[Required(ErrorMessageResourceName = "UsernameRequired", ErrorMessageResourceType = typeof(AppResources))]
[StringLength(150, MinimumLength = 6, ErrorMessageResourceName ="UsernameLength", ErrorMessageResourceType =typeof(AppResources))]
public string Username { get; set; }
[Display(Order = 2, Name = "Password", Prompt = "Password", ResourceType = typeof(AppResources))]
[Required(ErrorMessageResourceName ="PasswordRequired", ErrorMessageResourceType =typeof(AppResources))]
[StringLength(100, MinimumLength = 8, ErrorMessageResourceName = "Password", ErrorMessageResourceType = typeof(AppResources))]
[DataType(DataType.Password, ErrorMessageResourceName = "PasswordDataType", ErrorMessageResourceType = typeof(AppResources))]
public string Password { get; set; }
[Display(Name = "RememberMe", ResourceType = typeof(AppResources))]
public bool RememberMe { get; set; } = false;
}
There is way too much noise in this code. In real-world applications where your models may contain several properties, it becomes a challenge to work and maintain these constructs.
That's exactly what this library tries to address. You can keep your models clean, and define the annotations in a strongly typed manner in separate constructs. The library will analyze your definitions and produce adequate metadata constructs for your models. Practically, you'll get the best of both approaches, data annotations, and fluent-like configurations.
You indeed can manually create the metadata for your models. But, the maintenance is really hard and keeping them in sync is a tedious task. By using this library you'll get the following advantages:
ResourceType = typeof(YourResourceFileType)
for each and single attribute for all your properties in your model (if you're applying localization). It's even worse for the validation attributes, you have to provide ErrorMessageResourceType = typeof(YourResourceFileType))
and ErrorMessageResourceType = "ResourceKey"
. These definitions create a huge mess in your models. Now, you'll be able to define your resource file once, and it will be added for all the attributes automatically (wherever it is required).The library has support for NET Framework
and .NET
, and can be installed on both platforms. You can find the Nuget package as SmartAnnotations and you may install it through the Visual Studio package manager or the dotnet CLI.
System.ComponentModel
and System.ComponentModel.DataAnnotations
namespaces). This is no different from using the attributes manually. For .NET Framework
projects you may reference the required assembly or add the System.ComponentModel.Annotations
Nuget package. Same for the .NET
projects. This library just generates the content and does not constrain you with specific references. This is a deliberate decision, not to interfere with your configuration, so you can configure your dependencies based on your TFM.obj
directory.
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Sadly, there are few caveats, mostly related to the environment and the runtime support.
.NET Framework
projects. You can build your application through Visual Studio or CLI without issues..NET
projects, you will be able to successfully build with dotnet build
(I suggest you use dotnet clean | dotnet build
). But, the source generation won't work if you're building the project through Visual Studio. The issue is not related to msbuild, but the fact that VS is running on .NET Framework
and won't be able to load the .NET
assembly..NET Standard 2.0
projects. The data annotations are not part of .NET Standard
, and the corresponding Nuget package does not contain MetadataType
attribute for this TFM. There are few active issues on dotnet/runtime
, and it's unclear if this attribute will be added.The source generation feature is still in its infancy, and I do believe there will be much better support in the future.
The usage is quite straightforward, and we tried to provide a nice fluent API for building the annotations.
partial
Annotator<>
base class. We suggest you keep the models and the annotators in the same assembly.Sample model:
public partial class Foo
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Weight { get; set; }
}
Sample annotator for the given model:
public class FooAnnotator : Annotator<Foo>
{
public FooAnnotator()
{
Define().ResourceType(typeof(AppResources));
DefineFor(x => x.Id).ReadOnly(true);
DefineFor(x => x.Name).Required().Key("NameRequired")
.Display().Order(1).Name("NameKey")
.StringLenth(6, 150).Key("NameLength");
// You can define your resource file per attribute too.
DefineFor(x => x.Weight).Required().Message("This is my message, not a key to my resourceFile")
.Display(typeof(AppResources)).Order(2).Name("WeightKey")
.DisplayFormat().FormatString("{0:n2} Kg").ApplyFormatInEditMode();
}
}
Once you build the project, the following content will be autogenereted for the given model.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace SmartAnnotations.Sample
{
[MetadataType(typeof(FooMetaData))]
public partial class Foo
{
}
public class FooMetaData
{
[ReadOnly(true)]
public object Id;
[Display(Order = 1, Name = "NameKey", ResourceType = typeof(AppResources))]
[Required(ErrorMessageResourceName = "NameRequired", ErrorMessageResourceType = typeof(AppResources))]
[StringLength(150, MinimumLength = 6, ErrorMessageResourceName = "NameLength", ErrorMessageResourceType = typeof(AppResources))]
public object Name;
[Display(Order = 2, Name = "PriceX", ResourceType = typeof(AppResources))]
[Required(ErrorMessage = "This is my message, not a key to my resourceFile")]
[DisplayFormat(DataFormatString = "{0:n2} Kg", ApplyFormatInEditMode = false)]
public object Weight;
}
}
You can find more samples under sample
folder in this repository.
Initially, in v1.0.0, there is support for the following features and attributes.
If you like or are using this project please give it a star. Thanks!