Home

Awesome

<img align="left" src="docs/pozitronlogo.png" width="120" height="120">

  NuGetNuGet

  Build Status

  Azure DevOps coverage

 

SmartAnnotations

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.

Motivation

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. <strong>Practically, you'll get the best of both approaches, data annotations, and fluent-like configurations.</strong>

Why should I use this library instead of manually creating the metadata classes?

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:

Getting started

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.

<PropertyGroup>
	<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
	<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Important notice!

Sadly, there are few caveats, mostly related to the environment and the runtime support.

The source generation feature is still in its infancy, and I do believe there will be much better support in the future.

Usage

The usage is quite straightforward, and we tried to provide a nice fluent API for building the annotations.

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.

Features

Initially, in v1.0.0, there is support for the following features and attributes.

Give a Star! :star:

If you like or are using this project please give it a star. Thanks!