Restricción única en el código del marco de la entidad primero

125

Pregunta

¿Es posible definir una restricción única en una propiedad usando la sintaxis fluida o un atributo? Si no, ¿cuáles son las soluciones?

Tengo una clase de usuario con una clave principal, pero me gustaría asegurarme de que la dirección de correo electrónico también sea única. ¿Es esto posible sin editar la base de datos directamente?

Solución (basada en la respuesta de Matt)

public class MyContext : DbContext {
    public DbSet<User> Users { get; set; }

    public override int SaveChanges() {
        foreach (var item in ChangeTracker.Entries<IModel>())
            item.Entity.Modified = DateTime.Now;

        return base.SaveChanges();
    }

    public class Initializer : IDatabaseInitializer<MyContext> {
        public void InitializeDatabase(MyContext context) {
            if (context.Database.Exists() && !context.Database.CompatibleWithModel(false))
                context.Database.Delete();

            if (!context.Database.Exists()) {
                context.Database.Create();
                context.Database.ExecuteSqlCommand("alter table Users add constraint UniqueUserEmail unique (Email)");
            }
        }
    }
}
kim3er
fuente
1
Tenga en cuenta que hacer esto limita su aplicación a solo bases de datos que aceptan esa sintaxis exacta, en este caso, SQL Server. Si ejecuta su aplicación con un proveedor de Oracle, fallará.
DamienG
1
En esa situación, solo necesitaría crear una nueva clase Initializer, pero es un punto válido.
kim3er
3
Echa un vistazo a esta publicación: ValidationAttribute que valida un campo único contra sus otras filas en la base de datos , la solución se dirige a ObjectContexto DbContext.
Shimmy Weitzhandler
Sí, ahora es compatible desde EF 6.1 .
Evandro Pomatti

Respuestas:

61

Por lo que puedo decir, no hay forma de hacer esto con Entity Framework en este momento. Sin embargo, esto no es solo un problema con restricciones únicas ... es posible que desee crear índices, verificar restricciones y posiblemente disparadores y otras construcciones también. Aquí hay un patrón simple que puede usar con su configuración de código primero, aunque es cierto que no es independiente de la base de datos:

public class MyRepository : DbContext {
    public DbSet<Whatever> Whatevers { get; set; }

    public class Initializer : IDatabaseInitializer<MyRepository> {
        public void InitializeDatabase(MyRepository context) {
            if (!context.Database.Exists() || !context.Database.ModelMatchesDatabase()) {
                context.Database.DeleteIfExists();
                context.Database.Create();

                context.ObjectContext.ExecuteStoreCommand("CREATE UNIQUE CONSTRAINT...");
                context.ObjectContext.ExecuteStoreCommand("CREATE INDEX...");
                context.ObjectContext.ExecuteStoreCommand("ETC...");
            }
        }
    }
}

Otra opción es si su modelo de dominio es el único método para insertar / actualizar datos en su base de datos, podría implementar el requisito de exclusividad usted mismo y dejar la base de datos fuera de él. Esta es una solución más portátil y lo obliga a ser claro acerca de las reglas de su negocio en su código, pero deja su base de datos abierta a datos no válidos que se repasan.

mattmc3
fuente
Me gusta que mi DB sea tan ajustada como un tambor, la lógica se replica en la capa empresarial. Su respuesta solo funciona con CTP4 pero me puso en el camino correcto, he proporcionado una solución que es compatible con CTP5 debajo de mi pregunta original. ¡Muchas gracias!
kim3er
23
A menos que su aplicación sea de un solo usuario, creo que una restricción única es una cosa que no puede imponer con el código solo. Puede reducir drásticamente la probabilidad de una violación en el código (al verificar la unicidad antes de llamar SaveChanges()), pero aún existe la posibilidad de que otra inserción / actualización se deslice entre el momento de la verificación de la unicidad y el momento de SaveChanges(). Entonces, dependiendo de cuán crítica sea la misión de la aplicación y la probabilidad de una violación de unicidad, probablemente sea mejor agregar la restricción a la base de datos.
devuxer
1
Debería hacer que su cheque de unicidad sea parte de la misma transacción que sus SaveChanges. Asumiendo que su base de datos es compatible con ácidos, debería ser absolutamente capaz de imponer la singularidad de esta manera. Ahora, si EF le permite administrar adecuadamente el ciclo de vida de la transacción de esta manera, es otra cuestión.
mattmc3
1
@ mattmc3 Depende del nivel de aislamiento de su transacción. Solo el serializable isolation level(o bloqueo de tabla personalizado, ugh) realmente le permitiría garantizar la unicidad en su código. Pero la mayoría de las personas no lo usan serializable isolation levelpor razones de rendimiento. El valor predeterminado en MS Sql Server es read committed. Vea la serie de 4 partes a partir de: michaeljswart.com/2010/03/…
Nathan
3
EntityFramework 6.1.0 tiene soporte para IndexAttribute ahora, que básicamente puede agregarlo en la parte superior de las propiedades.
sotn
45

Comenzando con EF 6.1 ahora es posible:

[Index(IsUnique = true)]
public string EmailAddress { get; set; }

Esto le dará un índice único en lugar de una restricción única, estrictamente hablando. Para la mayoría de los propósitos prácticos son lo mismo .

Mihkel Müür
fuente
55
@Dave: simplemente use el mismo nombre de índice en los atributos de las propiedades respectivas ( fuente ).
Mihkel Müür
Tenga en cuenta que esto crea un índice único en lugar de una restricción única . Si bien son casi lo mismo, no son exactamente lo mismo (según tengo entendido, se pueden usar restricciones únicas como objetivo de un FK). Para una restricción, necesita ejecutar SQL.
Richard
(Después del último comentario) Otras fuentes sugieren que esta limitación se ha eliminado en versiones más recientes de SQL Server ... pero BOL no es completamente consistente.
Richard
@ Richard: las restricciones únicas basadas en atributos también son posibles (vea mi segunda respuesta ), aunque no fuera de la caja.
Mihkel Müür
1
@exSnake: desde SQL Server 2008, el índice único admite un único valor NULL por columna de forma predeterminada. En caso de que se requiera soporte para múltiples NULL, se necesitaría un índice filtrado. Consulte otra pregunta .
Mihkel Müür
28

No está realmente relacionado con esto, pero podría ayudar en algunos casos.

Si está buscando crear un índice compuesto único en, digamos, 2 columnas que actuarán como una restricción para su tabla, a partir de la versión 4.3 puede usar el nuevo mecanismo de migraciones para lograrlo:

Básicamente, debe insertar una llamada como esta en uno de sus scripts de migración:

CreateIndex("TableName", new string[2] { "Column1", "Column2" }, true, "IX_UniqueColumn1AndColumn2");

Algo como eso:

namespace Sample.Migrations
{
    using System;
    using System.Data.Entity.Migrations;

    public partial class TableName_SetUniqueCompositeIndex : DbMigration
    {
        public override void Up()
        {
            CreateIndex("TableName", new[] { "Column1", "Column2" }, true, "IX_UniqueColumn1AndColumn2");
        }

        public override void Down()
        {
            DropIndex("TableName", new[] { "Column1", "Column2" });
        }
    }
}
lnaie
fuente
Es bueno ver que EF tiene migraciones al estilo Rails. Ahora si solo pudiera ejecutarlo en Mono.
kim3er
2
¿No debería tener también un DropIndex en el procedimiento Down ()? DropIndex("TableName", new[] { "Column1", "Column2" });
Michael Bisbjerg
5

Hago un truco completo para ejecutar SQL cuando se crea la base de datos. Creo mi propio DatabaseInitializer y heredo de uno de los inicializadores proporcionados.

public class MyDatabaseInitializer : RecreateDatabaseIfModelChanges<MyDbContext>
{
    protected override void Seed(MyDbContext context)
    {
        base.Seed(context);
        context.Database.Connection.StateChange += new StateChangeEventHandler(Connection_StateChange);
    }

    void Connection_StateChange(object sender, StateChangeEventArgs e)
    {
        DbConnection cnn = sender as DbConnection;

        if (e.CurrentState == ConnectionState.Open)
        {
            // execute SQL to create indexes and such
        }

        cnn.StateChange -= Connection_StateChange;
    }
}

Ese es el único lugar que pude encontrar en mis declaraciones SQL.

Esto es de CTP4. No sé cómo funciona en CTP5.

Kelly Ethridge
fuente
Gracias Kelly! No estaba al tanto de ese controlador de eventos. Mi solución eventual coloca el SQL en el método InitializeDatabase.
kim3er
5

Solo tratando de averiguar si había una manera de hacer esto, solo la forma en que descubrí que hasta ahora lo estaba aplicando, creé un atributo para agregar a cada clase donde proporcionas el nombre de los campos que necesitas para ser único:

    [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=false,Inherited=true)]
public class UniqueAttribute:System.Attribute
{
    private string[] _atts;
    public string[] KeyFields
    {
        get
        {
            return _atts;
        }
    }
    public UniqueAttribute(string keyFields)
    {
        this._atts = keyFields.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);
    }
}

Luego en mi clase lo agregaré:

[CustomAttributes.Unique("Name")]
public class Item: BasePOCO
{
    public string Name{get;set;}
    [StringLength(250)]
    public string Description { get; set; }
    [Required]
    public String Category { get; set; }
    [Required]
    public string UOM { get; set; }
    [Required]
}

Finalmente, agregaré un método en mi repositorio, en el método Agregar o al guardar cambios como este:

private void ValidateDuplicatedKeys(T entity)
{
    var atts = typeof(T).GetCustomAttributes(typeof(UniqueAttribute), true);
    if (atts == null || atts.Count() < 1)
    {
        return;
    }
    foreach (var att in atts)
    {
        UniqueAttribute uniqueAtt = (UniqueAttribute)att;
        var newkeyValues = from pi in entity.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(entity, null).ToString() };
        foreach (var item in _objectSet)
        {
            var keyValues = from pi in item.GetType().GetProperties()
                            join k in uniqueAtt.KeyFields on pi.Name equals k
                            select new { KeyField = k, Value = pi.GetValue(item, null).ToString() };
            var exists = keyValues.SequenceEqual(newkeyValues);
            if (exists)
            {
                throw new System.Exception("Duplicated Entry found");
            }
        }
    }
}

No es muy agradable, ya que necesitamos confiar en la reflexión, ¡pero este es el enfoque que funciona para mí! = D

Rosendo
fuente
5

También en 6.1 puedes usar la versión de sintaxis fluida de la respuesta de @ mihkelmuur de esta manera:

Property(s => s.EmailAddress).HasColumnAnnotation(IndexAnnotation.AnnotationName,
new IndexAnnotation(
    new IndexAttribute("IX_UniqueEmail") { IsUnique = true }));

El método fluido no es IMO perfecto, pero al menos es posible ahora.

Más detalles en el blog de Arthur Vickers http://blog.oneunicorn.com/2014/02/15/ef-6-1-creating-indexes-with-indexattribute/

No amado
fuente
4

Una manera fácil en visual basic usando EF5 Code First Migrations

Muestra de clase pública

    Public Property SampleId As Integer

    <Required>
    <MinLength(1),MaxLength(200)>

    Public Property Code() As String

Clase final

El atributo MaxLength es muy importante para un índice único del tipo de cadena

Ejecute cmd: update-database -verbose

después de ejecutar cmd: agregar-migración 1

en el archivo generado

Public Partial Class _1
    Inherits DbMigration

    Public Overrides Sub Up()
        CreateIndex("dbo.Sample", "Code", unique:=True, name:="IX_Sample_Code")
    End Sub

    Public Overrides Sub Down()
        'DropIndex if you need it
    End Sub

End Class
Despota
fuente
Esta es en realidad una respuesta más apropiada que un inicializador de base de datos personalizado.
Shaun Wilson
4

Similar a la respuesta de Tobias Schittkowski pero C # y tiene la capacidad de tener múltiples campos en los constrtaints.

Para usar esto, simplemente coloque un [Único] en cualquier campo que desee que sea único. Para las cadenas, tendrá que hacer algo como (tenga en cuenta el atributo MaxLength):

[Unique]
[MaxLength(450)] // nvarchar(450) is max allowed to be in a key
public string Name { get; set; }

porque el campo de cadena predeterminado es nvarchar (max) y eso no se permitirá en una clave.

Para múltiples campos en la restricción puede hacer:

[Unique(Name="UniqueValuePairConstraint", Position=1)]
public int Value1 { get; set; }
[Unique(Name="UniqueValuePairConstraint", Position=2)]
public int Value2 { get; set; }

Primero, el atributo único:

/// <summary>
/// The unique attribute. Use to mark a field as unique. The
/// <see cref="DatabaseInitializer"/> looks for this attribute to 
/// create unique constraints in tables.
/// </summary>
internal class UniqueAttribute : Attribute
{
    /// <summary>
    /// Gets or sets the name of the unique constraint. A name will be 
    /// created for unnamed unique constraints. You must name your
    /// constraint if you want multiple fields in the constraint. If your 
    /// constraint has only one field, then this property can be ignored.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Gets or sets the position of the field in the constraint, lower 
    /// numbers come first. The order is undefined for two fields with 
    /// the same position. The default position is 0.
    /// </summary>
    public int Position { get; set; }
}

Luego, incluya una extensión útil para obtener el nombre de la tabla de la base de datos de un tipo:

public static class Extensions
{
    /// <summary>
    /// Get a table name for a class using a DbContext.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="type">
    /// The class to look up the table name for.
    /// </param>
    /// <returns>
    /// The table name; null on failure;
    /// </returns>
    /// <remarks>
    /// <para>
    /// Like:
    /// <code>
    ///   DbContext context = ...;
    ///   string table = context.GetTableName&lt;Foo&gt;();
    /// </code>
    /// </para>
    /// <para>
    /// This code uses ObjectQuery.ToTraceString to generate an SQL 
    /// select statement for an entity, and then extract the table
    /// name from that statement.
    /// </para>
    /// </remarks>
    public static string GetTableName(this DbContext context, Type type)
    {
        return ((IObjectContextAdapter)context)
               .ObjectContext.GetTableName(type);
    }

    /// <summary>
    /// Get a table name for a class using an ObjectContext.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    /// <param name="type">
    /// The class to look up the table name for.
    /// </param>
    /// <returns>
    /// The table name; null on failure;
    /// </returns>
    /// <remarks>
    /// <para>
    /// Like:
    /// <code>
    ///   ObjectContext context = ...;
    ///   string table = context.GetTableName&lt;Foo&gt;();
    /// </code>
    /// </para>
    /// <para>
    /// This code uses ObjectQuery.ToTraceString to generate an SQL 
    /// select statement for an entity, and then extract the table
    /// name from that statement.
    /// </para>
    /// </remarks>
    public static string GetTableName(this ObjectContext context, Type type)
    {
        var genericTypes = new[] { type };
        var takesNoParameters = new Type[0];
        var noParams = new object[0];
        object objectSet = context.GetType()
                            .GetMethod("CreateObjectSet", takesNoParameters)
                            .MakeGenericMethod(genericTypes)
                            .Invoke(context, noParams);
        var sql = (string)objectSet.GetType()
                  .GetMethod("ToTraceString", takesNoParameters)
                  .Invoke(objectSet, noParams);
        Match match = 
            Regex.Match(sql, @"FROM\s+(.*)\s+AS", RegexOptions.IgnoreCase);
        return match.Success ? match.Groups[1].Value : null;
    }
}

Luego, el inicializador de la base de datos:

/// <summary>
///     The database initializer.
/// </summary>
public class DatabaseInitializer : IDatabaseInitializer<PedContext>
{
    /// <summary>
    /// Initialize the database.
    /// </summary>
    /// <param name="context">
    /// The context.
    /// </param>
    public void InitializeDatabase(FooContext context)
    {
        // if the database has changed, recreate it.
        if (context.Database.Exists()
            && !context.Database.CompatibleWithModel(false))
        {
            context.Database.Delete();
        }

        if (!context.Database.Exists())
        {
            context.Database.Create();

            // Look for database tables in the context. Tables are of
            // type DbSet<>.
            foreach (PropertyInfo contextPropertyInfo in 
                     context.GetType().GetProperties())
            {
                var contextPropertyType = contextPropertyInfo.PropertyType;
                if (contextPropertyType.IsGenericType
                    && contextPropertyType.Name.Equals("DbSet`1"))
                {
                    Type tableType = 
                        contextPropertyType.GetGenericArguments()[0];
                    var tableName = context.GetTableName(tableType);
                    foreach (var uc in UniqueConstraints(tableType, tableName))
                    {
                        context.Database.ExecuteSqlCommand(uc);
                    }
                }
            }

            // this is a good place to seed the database
            context.SaveChanges();
        }
    }

    /// <summary>
    /// Get a list of TSQL commands to create unique constraints on the given 
    /// table. Looks through the table for fields with the UniqueAttribute
    /// and uses those and the table name to build the TSQL strings.
    /// </summary>
    /// <param name="tableClass">
    /// The class that expresses the database table.
    /// </param>
    /// <param name="tableName">
    /// The table name in the database.
    /// </param>
    /// <returns>
    /// The list of TSQL statements for altering the table to include unique 
    /// constraints.
    /// </returns>
    private static IEnumerable<string> UniqueConstraints(
        Type tableClass, string tableName)
    {
        // the key is the name of the constraint and the value is a list 
        // of (position,field) pairs kept in order of position - the entry
        // with the lowest position is first.
        var uniqueConstraints = 
            new Dictionary<string, List<Tuple<int, string>>>();
        foreach (PropertyInfo entityPropertyInfo in tableClass.GetProperties())
        {
            var unique = entityPropertyInfo.GetCustomAttributes(true)
                         .OfType<UniqueAttribute>().FirstOrDefault();
            if (unique != null)
            {
                string fieldName = entityPropertyInfo.Name;

                // use the name field in the UniqueAttribute or create a
                // name if none is given
                string constraintName = unique.Name
                                        ?? string.Format(
                                            "constraint_{0}_unique_{1}",
                                            tableName
                                               .Replace("[", string.Empty)
                                               .Replace("]", string.Empty)
                                               .Replace(".", "_"),
                                            fieldName);

                List<Tuple<int, string>> constraintEntry;
                if (!uniqueConstraints.TryGetValue(
                        constraintName, out constraintEntry))
                {
                    uniqueConstraints.Add(
                        constraintName, 
                        new List<Tuple<int, string>> 
                        {
                            new Tuple<int, string>(
                                unique.Position, fieldName) 
                        });
                }
                else
                {
                    // keep the list of fields in order of position
                    for (int i = 0; ; ++i)
                    {
                        if (i == constraintEntry.Count)
                        {
                            constraintEntry.Add(
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }

                        if (unique.Position < constraintEntry[i].Item1)
                        {
                            constraintEntry.Insert(
                                i, 
                                new Tuple<int, string>(
                                    unique.Position, fieldName));
                            break;
                        }
                    }
                }
            }
        }

        return
            uniqueConstraints.Select(
                uc =>
                string.Format(
                    "ALTER TABLE {0} ADD CONSTRAINT {1} UNIQUE ({2})",
                    tableName,
                    uc.Key,
                    string.Join(",", uc.Value.Select(v => v.Item2))));
    }
}
mheyman
fuente
2

Resolví el problema por reflexión (lo siento, amigos, VB.Net ...)

Primero, defina un atributo UniqueAttribute:

<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False, Inherited:=True)> _
Public Class UniqueAttribute
    Inherits Attribute

End Class

Luego, mejora tu modelo como

<Table("Person")> _
Public Class Person

    <Unique()> _
    Public Property Username() As String

End Class

Finalmente, cree un DatabaseInitializer personalizado (en mi versión, recreé la base de datos en los cambios de la base de datos solo si está en modo de depuración ...). En este DatabaseInitializer, los índices se crean automáticamente en función de los atributos únicos:

Imports System.Data.Entity
Imports System.Reflection
Imports System.Linq
Imports System.ComponentModel.DataAnnotations

Public Class DatabaseInitializer
    Implements IDatabaseInitializer(Of DBContext)

    Public Sub InitializeDatabase(context As DBContext) Implements IDatabaseInitializer(Of DBContext).InitializeDatabase
        Dim t As Type
        Dim tableName As String
        Dim fieldName As String

        If Debugger.IsAttached AndAlso context.Database.Exists AndAlso Not context.Database.CompatibleWithModel(False) Then
            context.Database.Delete()
        End If

        If Not context.Database.Exists Then
            context.Database.Create()

            For Each pi As PropertyInfo In GetType(DBContext).GetProperties
                If pi.PropertyType.IsGenericType AndAlso _
                    pi.PropertyType.Name.Contains("DbSet") Then

                    t = pi.PropertyType.GetGenericArguments(0)

                    tableName = t.GetCustomAttributes(True).OfType(Of TableAttribute).FirstOrDefault.Name
                    For Each piEntity In t.GetProperties
                        If piEntity.GetCustomAttributes(True).OfType(Of Model.UniqueAttribute).Any Then

                            fieldName = piEntity.Name
                            context.Database.ExecuteSqlCommand("ALTER TABLE " & tableName & " ADD CONSTRAINT con_Unique_" & tableName & "_" & fieldName & " UNIQUE (" & fieldName & ")")

                        End If
                    Next
                End If
            Next

        End If

    End Sub

End Class

Quizás esto ayude ...

Tobias Schittkowski
fuente
1

Si anula el método ValidateEntity en su clase DbContext, también puede poner la lógica allí. La ventaja aquí es que tendrá acceso completo a todos sus DbSets. Aquí hay un ejemplo:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Data.Entity.Validation;
using System.Linq;

namespace MvcEfClient.Models
{
    public class Location
    {
        [Key]
        public int LocationId { get; set; }

        [Required]
        [StringLength(50)]
        public string Name { get; set; }
    }

    public class CommitteeMeetingContext : DbContext
    {
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }

        protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
        {
            List<DbValidationError> validationErrors = new List<DbValidationError>();

            // Check for duplicate location names

            if (entityEntry.Entity is Location)
            {
                Location location = entityEntry.Entity as Location;

                // Select the existing location

                var existingLocation = (from l in Locations
                                        where l.Name == location.Name && l.LocationId != location.LocationId
                                        select l).FirstOrDefault();

                // If there is an existing location, throw an error

                if (existingLocation != null)
                {
                    validationErrors.Add(new DbValidationError("Name", "There is already a location with the name '" + location.Name + "'"));
                    return new DbEntityValidationResult(entityEntry, validationErrors);
                }
            }

            return base.ValidateEntity(entityEntry, items);
        }

        public DbSet<Location> Locations { get; set; }
    }
}
Frank Hoffman
fuente
1

Si está utilizando EF5 y todavía tiene esta pregunta, la solución a continuación lo resolvió por mí.

Estoy usando el primer enfoque de código, por lo tanto, pongo:

this.Sql("CREATE UNIQUE NONCLUSTERED INDEX idx_unique_username ON dbo.Users(Username) WHERE Username IS NOT NULL;");

en el script de migración hizo bien el trabajo. ¡También permite valores NULL!

FDIM
fuente
1

Con el enfoque EF Code First, se puede implementar un soporte de restricción único basado en atributos utilizando la siguiente técnica.

Crear un atributo marcador

[AttributeUsage(AttributeTargets.Property)]
public class UniqueAttribute : System.Attribute { }

Marque las propiedades que desea que sean únicas en las entidades, por ejemplo

[Unique]
public string EmailAddress { get; set; }

Cree un inicializador de base de datos o use uno existente para crear restricciones únicas

public class DbInitializer : IDatabaseInitializer<DbContext>
{
    public void InitializeDatabase(DbContext db)
    {
        if (db.Database.Exists() && !db.Database.CompatibleWithModel(false))
        {
            db.Database.Delete();
        }

        if (!db.Database.Exists())
        {
            db.Database.Create();
            CreateUniqueIndexes(db);
        }
    }

    private static void CreateUniqueIndexes(DbContext db)
    {
        var props = from p in typeof(AppDbContext).GetProperties()
                    where p.PropertyType.IsGenericType
                       && p.PropertyType.GetGenericTypeDefinition()
                       == typeof(DbSet<>)
                    select p;

        foreach (var prop in props)
        {
            var type = prop.PropertyType.GetGenericArguments()[0];
            var fields = from p in type.GetProperties()
                         where p.GetCustomAttributes(typeof(UniqueAttribute),
                                                     true).Any()
                         select p.Name;

            foreach (var field in fields)
            {
                const string sql = "ALTER TABLE dbo.[{0}] ADD CONSTRAINT"
                                 + " [UK_dbo.{0}_{1}] UNIQUE ([{1}])";
                var command = String.Format(sql, type.Name, field);
                db.Database.ExecuteSqlCommand(command);
            }
        }
    }   
}

Configure el contexto de su base de datos para usar este inicializador en el código de inicio (por ejemplo, en main()o Application_Start())

Database.SetInitializer(new DbInitializer());

La solución es similar a la de mheyman, con una simplificación de no admitir claves compuestas. Para ser utilizado con EF 5.0+.

Mihkel Müür
fuente
1

Solución fluida de Api:

modelBuilder.Entity<User>(entity =>
{
    entity.HasIndex(e => e.UserId)
          .HasName("IX_User")
          .IsUnique();

    entity.HasAlternateKey(u => u.Email);

    entity.HasIndex(e => e.Email)
          .HasName("IX_Email")
          .IsUnique();
});
Pierre
fuente
0

Hoy me enfrenté a ese problema y finalmente pude resolverlo. No sé si es un enfoque correcto, pero al menos puedo seguir adelante:

public class Person : IValidatableObject
{
    public virtual int ID { get; set; }
    public virtual string Name { get; set; }


    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var field = new[] { "Name" }; // Must be the same as the property

        PFContext db = new PFContext();

        Person person = validationContext.ObjectInstance as Person;

        var existingPerson = db.Persons.FirstOrDefault(a => a.Name == person.Name);

        if (existingPerson != null)
        {
            yield return new ValidationResult("That name is already in the db", field);
        }
    }
}
Juan carlos puerto
fuente
0

Use un validador de propiedad único.

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) {
   var validation_state = base.ValidateEntity(entityEntry, items);
   if (entityEntry.Entity is User) {
       var entity = (User)entityEntry.Entity;
       var set = Users;

       //check name unique
       if (!(set.Any(any_entity => any_entity.Name == entity.Name))) {} else {
           validation_state.ValidationErrors.Add(new DbValidationError("Name", "The Name field must be unique."));
       }
   }
   return validation_state;
}

ValidateEntityno se llama dentro de la misma transacción de la base de datos. Por lo tanto, puede haber condiciones de carrera con otras entidades en la base de datos. Tienes que hackear EF para forzar una transacción alrededor de SaveChanges(y, por lo tanto ValidateEntity). DBContextno puede abrir la conexión directamente, pero ObjectContextpuede.

using (TransactionScope transaction = new TransactionScope(TransactionScopeOption.Required)) {
   ((IObjectContextAdapter)data_context).ObjectContext.Connection.Open();
   data_context.SaveChanges();
   transaction.Complete();
}
Alex
fuente
0

Después de leer esta pregunta, tuve mi propia pregunta en el proceso de tratar de implementar un atributo para designar propiedades como claves únicas como las respuestas de Mihkel Müür , Tobias Schittkowski y mheyman sugieren: Asigna las propiedades del código del Marco de entidades a las columnas de la base de datos (CSpace a SSpace)

Finalmente llegué a esta respuesta, que puede asignar propiedades escalares y de navegación a las columnas de la base de datos y crear un índice único en una secuencia específica designada en el atributo. Este código asume que ha implementado un atributo único con una propiedad de secuencia, y lo aplicó a las propiedades de clase de entidad EF que deberían representar la clave única de la entidad (que no sea la clave primaria).

Nota: Este código se basa en EF versión 6.1 (o posterior) que expone que EntityContainerMappingno está disponible en versiones anteriores.

Public Sub InitializeDatabase(context As MyDB) Implements IDatabaseInitializer(Of MyDB).InitializeDatabase
    If context.Database.CreateIfNotExists Then
        Dim ws = DirectCast(context, System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext.MetadataWorkspace
        Dim oSpace = ws.GetItemCollection(Core.Metadata.Edm.DataSpace.OSpace)
        Dim entityTypes = oSpace.GetItems(Of EntityType)()
        Dim entityContainer = ws.GetItems(Of EntityContainer)(DataSpace.CSpace).Single()
        Dim entityMapping = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.EntitySetMappings
        Dim associations = ws.GetItems(Of EntityContainerMapping)(DataSpace.CSSpace).Single.AssociationSetMappings
        For Each setType In entityTypes
           Dim cSpaceEntitySet = entityContainer.EntitySets.SingleOrDefault( _
              Function(t) t.ElementType.Name = setType.Name)
           If cSpaceEntitySet Is Nothing Then Continue For ' Derived entities will be skipped
           Dim sSpaceEntitySet = entityMapping.Single(Function(t) t.EntitySet Is cSpaceEntitySet)
           Dim tableInfo As MappingFragment
           If sSpaceEntitySet.EntityTypeMappings.Count = 1 Then
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Single.Fragments.Single
           Else
              ' Select only the mapping (esp. PropertyMappings) for the base class
              tableInfo = sSpaceEntitySet.EntityTypeMappings.Where(Function(m) m.IsOfEntityTypes.Count _
                 = 1 AndAlso m.IsOfEntityTypes.Single.Name Is setType.Name).Single().Fragments.Single
           End If
           Dim tableName = If(tableInfo.StoreEntitySet.Table, tableInfo.StoreEntitySet.Name)
           Dim schema = tableInfo.StoreEntitySet.Schema
           Dim clrType = Type.GetType(setType.FullName)
           Dim uniqueCols As IList(Of String) = Nothing
           For Each propMap In tableInfo.PropertyMappings.OfType(Of ScalarPropertyMapping)()
              Dim clrProp = clrType.GetProperty(propMap.Property.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(propMap.Column.Name)
              End If
           Next
           For Each navProp In setType.NavigationProperties
              Dim clrProp = clrType.GetProperty(navProp.Name)
              If Attribute.GetCustomAttribute(clrProp, GetType(UniqueAttribute)) IsNot Nothing Then
                 Dim assocMap = associations.SingleOrDefault(Function(a) _
                    a.AssociationSet.ElementType.FullName = navProp.RelationshipType.FullName)
                 Dim sProp = assocMap.Conditions.Single
                 If uniqueCols Is Nothing Then uniqueCols = New List(Of String)
                 uniqueCols.Add(sProp.Column.Name)
              End If
           Next
           If uniqueCols IsNot Nothing Then
              Dim propList = uniqueCols.ToArray()
              context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IX_" & tableName & "_" & String.Join("_", propList) _
                 & " ON " & schema & "." & tableName & "(" & String.Join(",", propList) & ")")
           End If
        Next
    End If
End Sub
BlueMonkMN
fuente
0

Para aquellos que usan las primeras configuraciones de código, también pueden usar el objeto IndexAttribute como ColumnAnnotation y establecer su propiedad IsUnique en true.

Por ejemplo:

var indexAttribute = new IndexAttribute("IX_name", 1) {IsUnique = true};

Property(i => i.Name).HasColumnAnnotation("Index",new IndexAnnotation(indexAttribute));

Esto creará un índice único llamado IX_name en la columna Nombre.

Pascal Charbonneau
fuente
0

Perdón por la respuesta tardía, pero me pareció bueno compartirlo contigo

He publicado sobre esto en el proyecto de código

En general, depende de los atributos que pones en las clases para generar tus índices únicos

Wahid Bitar
fuente