Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ComponentModel attributes that assist in altering data for integrity #16953

Closed
roydukkey opened this issue Apr 11, 2016 · 3 comments
Closed

ComponentModel attributes that assist in altering data for integrity #16953

roydukkey opened this issue Apr 11, 2016 · 3 comments
Assignees
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.ComponentModel.DataAnnotations
Milestone

Comments

@roydukkey
Copy link
Contributor

It's been asked, how data annotations can change data, and replace or trim property values. I don't feel annotations should be used in this way, but there could be benefit in attributes that would preform common and simple tasks that do alter data.

These attributes might appropriately be called mutation attributes, which could include: Trim, Replace, RegexReplace, EnsureCase, etc.

Simple Example

It's common that models will trim whitespace form string properties and change empty strings to null.

public class User
{
    private string _firstName;

    public string FirstName {
        get {
            return _firstName;
        }
        set {
            value = value.Trim();

            _firstName = String.IsNullOrEmpty(value) ? null : value;
        }
    }
}

This could be the business requirement for all string properties of the user class. With mutation attributes this could be shortened drastically.

public class User
{
    [Trim, ToDefaultValue("")]
    public string FirstName { get; set; }
}

Advanced Example

Let's assume we have sign up screen that accepts a username and password, and a model that annotates User.UserName as accepting any alphanumeric string including underscores as valid characters. Should the end-user enter m@x_speed.01 as their desired username, validation would cause exceptions. Then the program should give an appropriate error message: "Username must contain only alphanumeric character and underscores".

However if the designer would provide mutation attributes, the program could try to determine a valid value from the input. We could then provide a better message to the user: "That username is invalid. Would you like to use 'mx_speed01' instead?"

Proposed API

namespace System.ComponentModel.DataMutations
{
    // Describes the context in which mutation is performed.
    // It supports IServiceProvider so that custom mutation code can acquire additional services to help it perform its mutation.
    public interface IMutationContext : IServiceProvider
    {
        object ObjectInstance { get; }
        IDictionary<object, object> Items { get; }
        IEnumerable<Attribute> Attributes { get; }
    }

    // Describes the context in which mutation is performed.
    // T: The type to consult during mutation.
    public sealed class MutationContext<T> : IMutationContext
    {
        // Constructors
        public MutationContext(T instance, IDictionary<object, object> items)
            : this(instance, items, null);
        public MutationContext(T instance, IServiceProvider serviceProvider)
            : this(instance, null, serviceProvider);
        public MutationContext(T instance, IDictionary<object, object> items, IServiceProvider serviceProvider)
            : this(instance);
        public MutationContext(T instance);

        // Properties
        public T ObjectInstance { get; }
        object IMutationContext.ObjectInstance { get; }
        public IDictionary<object, object> Items { get; }
        public IEnumerable<Attribute> Attributes { get; }

        // Methods
        public void InitializeServiceProvider(Func<Type, object> serviceProvider);
        public object GetService(Type serviceType);
    }

    // Helper class to validate objects, properties, and other values using their associated MutationAttributes and custom mutation as implemented through the IMutableObject interface.
    public static class Mutator
    {
        // Mutate Value
        public static T Mutate<T>(this MutationContext<T> context, IEnumerable<MutationAttribute> attributes)
            => typeof(T).GetTypeInfo().IsValueType
                ? // Mutate as value
                : // Mutate as object;
        public static T Mutate<T>(this MutationContext<T> context, IEnumerable<MutationAttribute> attributes, T value)
            where T : struct;

        // Mutate Object
        public static T Mutate<T>(this MutationContext<T> context)
            where T : class;
        public static T Mutate<T>(this MutationContext<T> context, T instance)
            where T : class;
        public static T Mutate<T>(this MutationContext<T> context, T instance, IEnumerable<MutationAttribute> attributes)
            where T : class;

        // Mutate Property
        public static object MutateProperty<T>(this MutationContext<T> context, PropertyInfo property)
            where T : class;
        public static P MutateProperty<T, P>(this MutationContext<T> context, PropertyInfo property, P value)
            where T : class;
        public static P MutateProperty<T, P>(this MutationContext<T> context, Expression<Func<T, P>> property)
            where T : class;
        public static P MutateProperty<T, P>(this MutationContext<T> context, Expression<Func<T, P>> property, P value)
            where T : class;
    }

    // Describes custom mutation logic that should be preformed on an object during mutation.
    public interface IMutableObject
    {
        void Mutate(IMutationContext context);
    }

    // Base class for all mutation attributes.
    public abstract class MutationAttribute : Attribute
    {
        // Properties
        public virtual bool RequiresContext { get; }
        public virtual int Priority { get; set; }

        // Methods
        public object Mutate(object value, IMutationContext context = null);
        protected abstract object MutateValue(object value, IMutationContext context);
    }

    // Trim Provides as sample implementation of MutationAttribute
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    public class TrimAttribute : MutationAttribute
    {
        // Constructors
        public TrimAttribute(params char[] characters);

        // Properties
        public char[] Characters { get; private set; }
        public TrimOptions Direction { get; set; }

        // Methods
        protected override object MutateValue(object value, IMutationContext context);
    }

}

Implemented as Proposed

Time has been spent on an implementation as it was needed for a project. Further documentation is provided on the repo. I will continue to update the repo according to this issue.

Source: roydukkey/Dado.ComponentModel.Mutations
MyGet: http://www.myget.org/gallery/roydukkey

Updates

  1. Add Attributes property to IMutationContext
  2. Add Priority property to MutationAttribute
    • Without a priority [DefaultValue("~/data/"), Trim, ToDefaultValue("")] string path { get; set; } = " "; will have a different result than [DefaultValue("~/data/"), ToDefaultValue(""), Trim] string path { get; set; } = " ";.
    • This will get even messier when inheritance is introduced.
@karelz
Copy link
Member

karelz commented Nov 16, 2016

@divega @rowanmiller @lajones do you think this one fits your area better?

@divega
Copy link
Contributor

divega commented Nov 16, 2016

@karelz I am not sure. Historically DataAnnotations have not been about mutating input data as much as about describing database mappings and constraints on the data (and performing validations based on those).

One possible exception that comes to mind is DisplayFormatAttribute.ConvertEmptyStringToNull, but even in that case the DataAnnotation only offers a hint on how the data should be interpreted, and the mutation is performed somewhere else, e.g. in MVC's model binding.

@karelz
Copy link
Member

karelz commented Nov 16, 2016

@roydukkey given that it doesn't seem to fit naturally into DataAnnotations direction and that you already have a NuGet library available for the APIs, we should probably leave it at that.
If we later find out the API is popular and there would be advantages from having it in BCL/CoreFX, we can reconsider.

Closing for now, feel free to reopen if you think I missed any major points.

@karelz karelz closed this as completed Nov 16, 2016
@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 2.0.0 milestone Jan 31, 2020
@dotnet dotnet locked as resolved and limited conversation to collaborators Jan 1, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.ComponentModel.DataAnnotations
Projects
None yet
Development

No branches or pull requests

5 participants