views:

372

answers:

4

I've been programming asp.net for, oh, a couple of days now. Here's a question I can't even begin to figure out for myself.

I hope it's obvious from the code what I want to accomplish, and I have, but it's not pretty. Furthermore I'd like to use it on whatever table, whatever field, i.e. check the uniqueness of a value against a table and field I specify, passing it all into the attribute constructor.

public class UniqueEmailAttribute : ValidationAttribute
{
    public UniqueEmailAttribute()
    {
    }

    public override Boolean IsValid(Object value)
    {
        //not pretty. todo: do away with this.
        var db = new CoinDataContext();
        int c = db.Emails.Count(e => e.Email1 == value.ToString());
        return (Boolean) (c == 0);
    }
}
A: 

I'm not into LINQ, but it seems you're trying to enforce uniqueness client side. That's just not possible. Uniqueness constraints must be enforced in the database. What do you think happens if a concurrent transaction commits an email address just after that check was done?

Even if you're checking just to provide a "Sorry, that address is already used"-message, there is still a possibility that another transaction inserts the same address.

Alex Brasetvik
You're correct ofcourse, but it's not to the point.
Martin
+2  A: 

First, let's look at rewriting the attribute...

public override bool IsValid(object value)
{
    var db = new CoinDataContext();

    //Return whether none of the email contains the specified value
    return db.Emails.Count(e => e.Email1 == value.ToString()) == 0;
}

Also, there was no need to cast (c == 0) as a boolean, as the result of that operation is already a bool. And the type bool is an alias for Boolean in the same way that int is an alias for Int32. Either is acceptable. I prefer the lower case version myself.

As Alex has already suggested in his answer, this wouldn't be a sure way of determining whether the email address was unique when it goes into the database. Only that it's unique at the time of checking.

Finally, and a bit off-tangent... I have written some linq extensions such as the following class. Using it would allow me to rewrite the return on the attribute to db.Emails.None(e => e.Email1 == value.ToString());. This makes it a little bit more readable.

Update There isn't a way of determining the uniqueness of a value in the database without going to the database and comparing the rows against the values written. You still need to create an instance to the database. What I would do though is look at seperating these concerns into areas such as a service layer and a data layer (separate projects from the MVC website project). Your data layer would exclusively handle anything to do with the database. If you'd like I can write some examples of how you'd separate the CoinDataContext from the attribute itself?

Addressing another of your concerns, here we remove the need for the query inside the attribute, but you still need a call to the database, and specifying which table you want to use.

Because this is an attribute however, I'm not 100% sure if you can use linq lambda expressions in the attribute it, so your attribute has to remain generalised in this fashion.

Data layer project

This layer would contain different classes relating to different tables. The class below is dedicated to the email table.

Email Mapper class

public static class EmailMapper
{
  public static void IsValid(Func<string, bool> query)
  {
    var db = new CoinDataContext();
    return db.Emails.Count(query) == 0;
  }
}

Service layer project

This layer is responsible for general validation of objects, but also is used for going to other layers such as external APIs.

EmailService class

public static class EmailService
{
  public static IsValid(string address)
  {
    bool isValid = false;

    //...Check email is valid first with regex. Not done.
    isValid = RegexHelper.IsEmailAddressValid(address);

    //Go to the database and determine it's valid ONLY if the regex passes.
    return isValid ? EmailMapper.IsValid(x=> x.Email == address) : false;
  }
}

Attribute class in web project

public override Boolean IsValid(Object value)
{
    return EmailService.IsValid(value.ToString());
}
Dan Atkinson
Thank you for your answer. Not quite what I was looking for though. Your linq extensions will be very useful for me, great work.I was hoping for an IsValid without CoinDataContext and without Email or Email1 being mention. Calling it UniqueAttribute, not UniqueEmailAttribute. Super generalized.
Martin
I moved the class to http://www.extensionmethod.net/Details.aspx?ID=258 as it doesn't really belong in this answer. Also updated for some more explanation.
Dan Atkinson
@Martin: The best way to do that is to pull the data context out of the attribute itself and consider a separation of concerns approach, creating a data layer for any database-related functionality, and a service layer in order to validate anything before going off to the database.
Dan Atkinson
I like the sound of this, that's exactly what I'm getting at. If you'd give me an example, that would be absolutely terrific, provided you think you can do it linq to sql style. If you need to access the database directly, then I can manage on my own.
Martin
Dan, you're terrific! Now you've tought me Func<string, bool> query and a about layering my code, totalling three really useful things I need. But still, the attribute still only checks emails and only in coindatacontext. Hang on though, I'v got a partial answer on the asp.net forum. I'll post it here when it's complete.
Martin
+2  A: 

This just in from asp.net forums by Brad Wilson. So pleased with it. No error handling!

using System;
using System.ComponentModel.DataAnnotations;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public class UniqueAttribute : ValidationAttribute {
    public UniqueAttribute(Type dataContextType, Type entityType, string propertyName) {
        DataContextType = dataContextType;
        EntityType = entityType;
        PropertyName = propertyName;
    }

    public Type DataContextType { get; private set; }

    public Type EntityType { get; private set; }

    public string PropertyName { get; private set; }

    public override bool IsValid(object value) {
        // Construct the data context
        ConstructorInfo constructor = DataContextType.GetConstructor(new Type[0]);
        DataContext dataContext = (DataContext)constructor.Invoke(new object[0]);

        // Get the table
        ITable table = dataContext.GetTable(EntityType);

        // Get the property
        PropertyInfo propertyInfo = EntityType.GetProperty(PropertyName);

        // Our ultimate goal is an expression of:
        //   "entity => entity.PropertyName == value"

        // Expression: "value"
        object convertedValue = Convert.ChangeType(value, propertyInfo.PropertyType);
        ConstantExpression rhs = Expression.Constant(convertedValue);

        // Expression: "entity"
        ParameterExpression parameter = Expression.Parameter(EntityType, "entity");

        // Expression: "entity.PropertyName"
        MemberExpression property = Expression.MakeMemberAccess(parameter, propertyInfo);

        // Expression: "entity.PropertyName == value"
        BinaryExpression equal = Expression.Equal(property, rhs);

        // Expression: "entity => entity.PropertyName == value"
        LambdaExpression lambda = Expression.Lambda(equal, parameter);

        // Instantiate the count method with the right TSource (our entity type)
        MethodInfo countMethod = QueryableCountMethod.MakeGenericMethod(EntityType);

        // Execute Count() and say "you're valid if you have none matching"
        int count = (int)countMethod.Invoke(null, new object[] { table, lambda });
        return count == 0;
    }

    // Gets Queryable.Count<TSource>(IQueryable<TSource>, Expression<Func<TSource, bool>>)
    private static MethodInfo QueryableCountMethod = typeof(Queryable).GetMethods().First(m => m.Name == "Count" && m.GetParameters().Length == 2);
}
Martin
+1  A: 

Could you please tell me how to implement unique property validation on edited entity? In this case the unique attribute implementation will fail because it doesn't take into account the entity being edited.

It is possible to rewrite this attribute to use it on the entity level, but then the error will belong to entity level, not the property.

UI developer
I'm not skilled enough to to give you a reliable answer. I suggest you start a new thread and reference this question. Good luck!
Martin