I'm running EF 4.2 CF and want to create indexes on certain columns in my POCO objects.
As an example lets say we have this employee class:
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeCode { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime HireDate { get; set; }
}
We often do searches for employees by their EmployeeCode and since there are a lot of employees it would be nice to have that indexed for performance reasons.
Can we do this with fluent api somehow? or perhaps data annotations?
I know it is possible to execute sql commands something like this:
context.Database.ExecuteSqlCommand("CREATE INDEX IX_NAME ON ...");
I would very much like to avoid raw SQL like that.
i know this does not exist but looking for something along those lines:
class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
internal EmployeeConfiguration()
{
this.HasIndex(e => e.EmployeeCode)
.HasIndex(e => e.FirstName)
.HasIndex(e => e.LastName);
}
}
or maybe using System.ComponentModel.DataAnnotations the POCO could look like this (again i know this does not exist):
public class Employee
{
public int EmployeeID { get; set; }
[Indexed]
public string EmployeeCode { get; set; }
[Indexed]
public string FirstName { get; set; }
[Indexed]
public string LastName { get; set; }
public DateTime HireDate { get; set; }
}
Anyone have any ideas on how to do this, or if there are any plans to implement a way to do this, the code first way?
UPDATE: As mentioned in the answer by Robba, this feature is implemented in EF version 6.1
After Migrations was introduced in EF 4.3 you can now add indexes when modifying or creating a table. Here is an excerpt from the EF 4.3 Code-Based Migrations Walkthrough from the ADO.NET team blog
namespace MigrationsCodeDemo.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddPostClass : DbMigration
{
public override void Up()
{
CreateTable(
"Posts",
c => new
{
PostId = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 200),
Content = c.String(),
BlogId = c.Int(nullable: false),
})
.PrimaryKey(t => t.PostId)
.ForeignKey("Blogs", t => t.BlogId, cascadeDelete: true)
.Index(t => t.BlogId)
.Index(p => p.Title, unique: true);
AddColumn("Blogs", "Rating", c => c.Int(nullable: false, defaultValue: 3));
}
public override void Down()
{
DropIndex("Posts", new[] { "BlogId" });
DropForeignKey("Posts", "BlogId", "Blogs");
DropColumn("Blogs", "Rating");
DropTable("Posts");
}
}
}
This is a nice strongly typed way to add the indexes, which was what i was looking for when i first posted the question.
You could create an attribute called indexed (as you suggested), which is then picked up in a custom initializer.
I created the following attribute:
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class IndexAttribute : Attribute
{
public IndexAttribute(bool isUnique = false, bool isClustered = false, SortOrder sortOrder = SortOrder.Ascending)
{
IsUnique = isUnique;
IsClustered = isClustered;
SortOrder = sortOrder == SortOrder.Unspecified ? SortOrder.Ascending : sortOrder;
}
public bool IsUnique { get; private set; }
public bool IsClustered { get; private set; }
public SortOrder SortOrder { get; private set; }
//public string Where { get; private set; }
}
I then created a custom initializer which got a list of the table names created for the entities in my context. I have two base classes which all my entities inherit, so I did the following to get the table names:
var baseEF = typeof (BaseEFEntity);
var baseLink = typeof (BaseLinkTable);
var types =
AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(s => s.GetTypes()).Where(
baseEF.IsAssignableFrom).Union(AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(
s => s.GetTypes()).Where(
baseLink.IsAssignableFrom));
var sqlScript = context.ObjectContext.CreateDatabaseScript();
foreach (var type in types)
{
var table = (TableAttribute) type.GetCustomAttributes(typeof (TableAttribute), true).FirstOrDefault();
var tableName = (table != null ? table.Name : null) ?? Pluralizer.Pluralize(type.Name);
I then found all the properties on each entity that have this attribute and then execute a SQL command to generate the index on each property. Sweet!
//Check that a table exists
if (sqlScript.ToLower().Contains(string.Format(CREATETABLELOOKUP, tableName.ToLower())))
{
//indexes
var indexAttrib = typeof (IndexAttribute);
properties = type.GetProperties().Where(prop => Attribute.IsDefined(prop, indexAttrib));
foreach (var property in properties)
{
var attributes = property.GetCustomAttributes(indexAttrib, true).ToList();
foreach (IndexAttribute index in attributes)
{
var indexName = string.Format(INDEXNAMEFORMAT, tableName, property.Name,
attributes.Count > 1
? UNDERSCORE + (attributes.IndexOf(index) + 1)
: string.Empty);
try
{
context.ObjectContext.ExecuteStoreCommand(
string.Format(INDEX_STRING, indexName,
tableName,
property.Name,
index.IsUnique ? UNIQUE : string.Empty,
index.IsClustered ? CLUSTERED : NONCLUSTERED,
index.SortOrder == SortOrder.Ascending ? ASC : DESC));
}
catch (Exception)
{
}
}
}
I even went on to add class based indexes (which could have multiple columns) , unique constraints and default constraints all in the same way. Whats also really nice is that if you put these attributes on an inherited class the index or constraint gets applied to all the classes (tables) that inherit it.
BTW the pluralizer helper contains the following:
public static class Pluralizer
{
private static object _pluralizer;
private static MethodInfo _pluralizationMethod;
public static string Pluralize(string word)
{
CreatePluralizer();
return (string) _pluralizationMethod.Invoke(_pluralizer, new object[] {word});
}
public static void CreatePluralizer()
{
if (_pluralizer == null)
{
var aseembly = typeof (DbContext).Assembly;
var type =
aseembly.GetType(
"System.Data.Entity.ModelConfiguration.Design.PluralizationServices.EnglishPluralizationService");
_pluralizer = Activator.CreateInstance(type, true);
_pluralizationMethod = _pluralizer.GetType().GetMethod("Pluralize");
}
}
}
To build on frozen's response, you can hand code it into a migration yourself.
First, go to the Package Manager Console and create a new migration with add-migration, then give it a name. A blank migration will appear. Stick this in:
public override void Up()
{
CreateIndex("TableName", "ColumnName");
}
public override void Down()
{
DropIndex("TableName",new[] {"ColumnName"});
}
Note that if you're using a string field it needs to be capped to a length of 450 chars as well.
I've also looked into this recently and found no other way, so I settled with creating indexes when seeding the database:
public class MyDBInitializer : DropCreateDatabaseIfModelChanges<MyContext>
{
private MyContext _Context;
protected override void Seed(MyContext context)
{
base.Seed(context);
_Context = context;
// We create database indexes
CreateIndex("FieldName", typeof(ClassName));
context.SaveChanges();
}
private void CreateIndex(string field, Type table)
{
_Context.Database.ExecuteSqlCommand(String.Format("CREATE INDEX IX_{0} ON {1} ({0})", field, table.Name));
}
}
Note that in Entity Framework 6.1 (currently in beta) will support the IndexAttribute to annotate the index properties which will automatically result in a (unique) index in your Code First Migrations.
For anyone using Entity Framework 6.1+, you can do the following with fluent api:
modelBuilder
.Entity<Department>()
.Property(t => t.Name)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));
Read more in the documentation.
Well i found a solution online and adapted it to fit my needs here it is:
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class IndexAttribute : Attribute
{
public IndexAttribute(string name, bool unique = false)
{
this.Name = name;
this.IsUnique = unique;
}
public string Name { get; private set; }
public bool IsUnique { get; private set; }
}
public class IndexInitializer<T> : IDatabaseInitializer<T> where T : DbContext
{
private const string CreateIndexQueryTemplate = "CREATE {unique} INDEX {indexName} ON {tableName} ({columnName});";
public void InitializeDatabase(T context)
{
const BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance;
Dictionary<IndexAttribute, List<string>> indexes = new Dictionary<IndexAttribute, List<string>>();
string query = string.Empty;
foreach (var dataSetProperty in typeof(T).GetProperties(PublicInstance).Where(p => p.PropertyType.Name == typeof(DbSet<>).Name))
{
var entityType = dataSetProperty.PropertyType.GetGenericArguments().Single();
TableAttribute[] tableAttributes = (TableAttribute[])entityType.GetCustomAttributes(typeof(TableAttribute), false);
indexes.Clear();
string tableName = tableAttributes.Length != 0 ? tableAttributes[0].Name : dataSetProperty.Name;
foreach (PropertyInfo property in entityType.GetProperties(PublicInstance))
{
IndexAttribute[] indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])property.GetCustomAttributes(typeof(NotMappedAttribute), false);
if (indexAttributes.Length > 0 && notMappedAttributes.Length == 0)
{
ColumnAttribute[] columnAttributes = (ColumnAttribute[])property.GetCustomAttributes(typeof(ColumnAttribute), false);
foreach (IndexAttribute indexAttribute in indexAttributes)
{
if (!indexes.ContainsKey(indexAttribute))
{
indexes.Add(indexAttribute, new List<string>());
}
if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
{
string columnName = columnAttributes.Length != 0 ? columnAttributes[0].Name : property.Name;
indexes[indexAttribute].Add(columnName);
}
else
{
indexes[indexAttribute].Add(property.PropertyType.Name + "_" + GetKeyName(property.PropertyType));
}
}
}
}
foreach (IndexAttribute indexAttribute in indexes.Keys)
{
query += CreateIndexQueryTemplate.Replace("{indexName}", indexAttribute.Name)
.Replace("{tableName}", tableName)
.Replace("{columnName}", string.Join(", ", indexes[indexAttribute].ToArray()))
.Replace("{unique}", indexAttribute.IsUnique ? "UNIQUE" : string.Empty);
}
}
if (context.Database.CreateIfNotExists())
{
context.Database.ExecuteSqlCommand(query);
}
}
private string GetKeyName(Type type)
{
PropertyInfo[] propertyInfos = type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public);
foreach (PropertyInfo propertyInfo in propertyInfos)
{
if (propertyInfo.GetCustomAttribute(typeof(KeyAttribute), true) != null)
return propertyInfo.Name;
}
throw new Exception("No property was found with the attribute Key");
}
}
Then overload OnModelCreating in your dbcontext
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer(new IndexInitializer<MyContext>());
base.OnModelCreating(modelBuilder);
}
Apply the index attribute to your Entity type, with this solution you can have multiple fields in the same index just use the same name and unique.
Extending Tsuushin's answer above to support multiple columns and unique constraints:
private void CreateIndex(RBPContext context, string field, string table, bool unique = false)
{
context.Database.ExecuteSqlCommand(String.Format("CREATE {0}NONCLUSTERED INDEX IX_{1}_{2} ON {1} ({3})",
unique ? "UNIQUE " : "",
table,
field.Replace(",","_"),
field));
}
expanding on Petoj
i modified the CreateIndexQueryTemplate to
private const string CreateIndexQueryTemplate = "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '{indexName}') CREATE {unique} INDEX {indexName} ON {tableName} ({columnName});";
and removed the following from OnModelCreating
Database.SetInitializer(new IndexInitializer<MyContext>());
and added the following to Configuration Seeding method
new IndexInitializer<MyContext>().InitializeDatabase(context);
this way the index attributes are run every time you do a update-database.
If you want this feature added to EF then you can vote for it here http://entityframework.codeplex.com/workitem/57
jwsadler's extension of Data Annotations was a nice fit for us. We use Annotations to influence the treatment of a class or property and Fluent API for global changes.
Our annotations cover indexes (unique and not unique) plus default values of getdate() and (1). The code sample shows how we applied it to our situation. All of our classes inherit from one base class. This implementation makes a lot of assumptions because we have a pretty simple model. We're using Entity Framework 6.0.1. Lots of comments have been included.
using System;
using System.Linq;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
namespace YourNameSpace
{
public enum SqlOption
{
Active = 1,
GetDate = 2,
Index = 3,
Unique = 4,
}
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public class SqlAttribute : Attribute
{
public SqlAttribute(SqlOption selectedOption = SqlOption.Index)
{
this.Option = selectedOption;
}
public SqlOption Option {get; set;}
}
// See enum above, usage examples: [Sql(SqlOption.Unique)] [Sql(SqlOption.Index)] [Sql(SqlOption.GetDate)]
public class SqlInitializer<T> : IDatabaseInitializer<T> where T : DbContext
{
// Create templates for the DDL we want generate
const string INDEX_TEMPLATE = "CREATE NONCLUSTERED INDEX IX_{columnName} ON [dbo].[{tableName}] ([{columnName}]);";
const string UNIQUE_TEMPLATE = "CREATE UNIQUE NONCLUSTERED INDEX UQ_{columnName} ON [dbo].[{tableName}] ([{columnName}]);";
const string GETDATE_TEMPLATE = "ALTER TABLE [dbo].[{tableName}] ADD DEFAULT (getdate()) FOR [{columnName}];";
const string ACTIVE_TEMPLATE = "ALTER TABLE [dbo].[{tableName}] ADD DEFAULT (1) FOR [{columnName}];";
// Called by Database.SetInitializer(new IndexInitializer< MyDBContext>()); in MyDBContext.cs
public void InitializeDatabase(T context)
{
// To be used for the SQL DDL that I generate
string sql = string.Empty;
// All of my classes are derived from my base class, Entity
var baseClass = typeof(Entity);
// Get a list of classes in my model derived from my base class
var modelClasses = AppDomain.CurrentDomain.GetAssemblies().ToList().
SelectMany(s => s.GetTypes()).Where(baseClass.IsAssignableFrom);
// For debugging only - examine the SQL DDL that Entity Framework is generating
// Manipulating this is discouraged.
var generatedDDSQL = ((IObjectContextAdapter)context).ObjectContext.CreateDatabaseScript();
// Define which Annotation Attribute we care about (this class!)
var annotationAttribute = typeof(SqlAttribute);
// Generate a list of concrete classes in my model derived from
// Entity class since we follow Table Per Concrete Class (TPC).
var concreteClasses = from modelClass in modelClasses
where !modelClass.IsAbstract
select modelClass;
// Iterate through my model's concrete classes (will be mapped to tables)
foreach (var concreteClass in concreteClasses)
{
// Calculate the table name - could get the table name from list of DbContext's properties
// to be more correct (but this is sufficient in my case)
var tableName = concreteClass.Name + "s";
// Get concrete class's properties that have this annotation
var propertiesWithAnnotations = concreteClass.GetProperties().Where(prop => Attribute.IsDefined(prop, annotationAttribute));
foreach (var annotatedProperty in propertiesWithAnnotations)
{
var columnName = annotatedProperty.Name;
var annotationProperties = annotatedProperty.GetCustomAttributes(annotationAttribute, true).ToList();
foreach (SqlAttribute annotationProperty in annotationProperties)
{
// Generate the appropriate SQL DLL based on the attribute selected
switch (annotationProperty.Option)
{
case SqlOption.Active: // Default value of true plus an index (for my case)
sql += ACTIVE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.GetDate: // GetDate plus an index (for my case)
sql += GETDATE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.Index: // Default for empty annotations for example [Sql()]
sql += INDEX_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
case SqlOption.Unique:
sql += UNIQUE_TEMPLATE.Replace("{tableName}", tableName).Replace("{columnName}", columnName);
break;
} // switch
} // foreach annotationProperty
} // foreach annotatedProperty
} // foreach concreteClass
// Would have been better not to go through all the work of generating the SQL
// if we weren't going to use it, but putting it here makes it easier to follow.
if (context.Database.CreateIfNotExists())
context.Database.ExecuteSqlCommand(sql);
} // InitializeDatabase
} // SqlInitializer
} // Namespace
Here's our context:
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace YourNameSpace
{
public class MyDBContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Only including my concrete classes here as we're following Table Per Concrete Class (TPC)
public virtual DbSet<Attendance> Attendances { get; set; }
public virtual DbSet<Course> Courses { get; set; }
public virtual DbSet<Location> Locations { get; set; }
public virtual DbSet<PaymentMethod> PaymentMethods { get; set; }
public virtual DbSet<Purchase> Purchases { get; set; }
public virtual DbSet<Student> Students { get; set; }
public virtual DbSet<Teacher> Teachers { get; set; }
// Process the SQL Annotations
Database.SetInitializer(new SqlInitializer<MyDBContext>());
base.OnModelCreating(modelBuilder);
// Change all datetime columns to datetime2
modelBuilder.Properties<DateTime>().Configure(c => c.HasColumnType("datetime2"));
// Turn off cascading deletes
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
}
}
To build further on all these great responses, we added the following code to enable the Index attribute to be picked up from an associated metadata type. For the full details please see my blog post, but in summary here are the details.
Metadata types are used like this:
[MetadataType(typeof(UserAccountAnnotations))]
public partial class UserAccount : IDomainEntity
{
[Key]
public int Id { get; set; } // Unique ID
sealed class UserAccountAnnotations
{
[Index("IX_UserName", unique: true)]
public string UserName { get; set; }
}
}
In this example the metadata type is a nested class, but it doesn't have to be, it can be any type. Property matching is done by name only, so the metadata type just has to have a property of the same name, and any data annotations applied to that should then be applied to the associated entity class. This didn't work in the original solution because it doesn't check for the associated metadata type. We plumbed in the following helper method:
/// <summary>
/// Gets the index attributes on the specified property and the same property on any associated metadata type.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>IEnumerable{IndexAttribute}.</returns>
IEnumerable<IndexAttribute> GetIndexAttributes(PropertyInfo property)
{
Type entityType = property.DeclaringType;
var indexAttributes = (IndexAttribute[])property.GetCustomAttributes(typeof(IndexAttribute), false);
var metadataAttribute =
entityType.GetCustomAttribute(typeof(MetadataTypeAttribute)) as MetadataTypeAttribute;
if (metadataAttribute == null)
return indexAttributes; // No metadata type
Type associatedMetadataType = metadataAttribute.MetadataClassType;
PropertyInfo associatedProperty = associatedMetadataType.GetProperty(property.Name);
if (associatedProperty == null)
return indexAttributes; // No metadata on the property
var associatedIndexAttributes =
(IndexAttribute[])associatedProperty.GetCustomAttributes(typeof(IndexAttribute), false);
return indexAttributes.Union(associatedIndexAttributes);
}
For EF7 you can use the hasIndex() method.
We can set clustered and non-clustered index as well.
By default primary key will be clustered . We can change that behavior too.
supplierItemEntity.HasKey(supplierItem => supplierItem.SupplierItemId).ForSqlServerIsClustered(false);
supplierItemEntity.HasIndex(s => new { s.ItemId }).ForSqlServerIsClustered(true);
I discovered a problem with the answer #highace gave - the down migration uses the wrong override for DropIndex. Here is what I did:
To comply with Sql Server's limitation on index columns (900 bytes) I reduced the size of a couple of fields in my model
I added the migration using Add-Migration "Add Unique Indexes"
I manually added the CreateIndex and DropIndex methods to the migration. I used the override that takes the index name for the single column index. I used the override that takes an array of column names where the index spans more than one column
And here is the code with examples of both overrides of each method:
public partial class AddUniqueIndexes : DbMigration
{
public override void Up()
{
//Sql Server limits indexes to 900 bytes,
//so we need to ensure cumulative field sizes do not exceed this
//otherwise inserts and updates could be prevented
//http://www.sqlteam.com/article/included-columns-sql-server-2005
AlterColumn("dbo.Answers",
"Text",
c => c.String(nullable: false, maxLength: 400));
AlterColumn("dbo.ConstructionTypes",
"Name",
c => c.String(nullable: false, maxLength: 300));
//[IX_Text] is the name that Entity Framework would use by default
// even if it wasn't specified here
CreateIndex("dbo.Answers",
"Text",
unique: true,
name: "IX_Text");
//Default name is [IX_Name_OrganisationID]
CreateIndex("dbo.ConstructionTypes",
new string[] { "Name", "OrganisationID" },
unique: true);
}
public override void Down()
{
//Drop Indexes before altering fields
//(otherwise it will fail because of dependencies)
//Example of dropping an index based on its name
DropIndex("dbo.Answers", "IX_Text");
//Example of dropping an index based on the columns it targets
DropIndex("dbo.ConstructionTypes",
new string[] { "Name", "OrganisationID" });
AlterColumn("dbo.ConstructionTypes",
"Name",
c => c.String(nullable: false));
AlterColumn("dbo.Answers",
"Text",
c => c.String(nullable: false, maxLength: 500));
}
You can specify index in ModelBuilder
modelBuilder
.Entity<UserSalary>(builder =>
{
builder.HasNoKey();
builder.HasIndex("UserId").IsUnique(false);
builder.ToTable("UserSalary");
});
Related
I'm using automapper via DI and want to have generic rules for all the mappings found in my solution. Belows example is of a single class, however I've got hundreds of classes to maintain and therefore want to add it to the mapper, not the mapping profile.
So far, I can handle Null values as follows :
cnfg.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null)));
Which in my example below, allows for the null update to persist past all the subsiquent updates. What I want is a generic rule ( or statement) for the default value of Guids, DateTimeOffset and Int. In my example below, update 1 and 2 are lost once update 3 is mapped.
using System;
using Autofac;
using AutoMapper;
namespace AutoMapperProblem
{
public class Program
{
static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterInstance(new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(Program).Assembly);
cfg.ForAllMaps((obj, cnfg) =>
cnfg.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null)));
}).CreateMapper()).As<IMapper>().SingleInstance();
var container = builder.Build();
var mapper = container.Resolve<IMapper>();
var MainObject = new MainObject();
//this update persists pass all updates
var updateNull = new UpdateObject { NullGuid = Guid.NewGuid() };
mapper.Map(updateNull, MainObject);
var update1 = new UpdateObject { DateTimeOffset = DateTimeOffset.Now };
mapper.Map(update1, MainObject);
var update2 = new UpdateObject { Guid = Guid.NewGuid() };
mapper.Map(update2, MainObject);
var update3 = new UpdateObject { Int = 10 };
mapper.Map(update3, MainObject);
}
}
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<MainObject, UpdateObject>()
.ReverseMap();
}
}
public class MainObject
{
public Guid? NullGuid { get; set; }
public Guid Guid { get; set; }
public int Int { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
}
public class UpdateObject
{
public Guid? NullGuid { get; set; }
public Guid Guid { get; set; }
public int Int { get; set; }
public DateTimeOffset DateTimeOffset { get; set; }
}
}
I want it to work based on the type, eg ints, DateTimeOffsets and Guid's .
Heres a working example on DotNetFiddle : https://dotnetfiddle.net/Zh0ta6
Thanks for any help with this
The problem is that you cannot check value types against null in your opts.Condition() check, as they always have a value. Also, you can't use the default keyword directly in this specific context because the variable type is object. That will turn default to null again, and fail.
Depending on what you want to do, you can check if the destination already have a non-default value and do NOT replace it with any source value, which might be the default value of the type (0 for int, 0000...-000 for Guid, etc.). The check could look like this:
cnfg.ForAllMembers(opts => opts.Condition((src, dest, srcMember, destMember) =>
{
object destDefaultValue = null;
if (destMember != null)
{
Type destType = destMember.GetType();
if (destType.IsValueType)
{
destDefaultValue = Activator.CreateInstance(destType);
}
else
{
destDefaultValue = null;
}
}
bool destinationHasNoValue = Object.Equals(destMember, destDefaultValue);
return destinationHasNoValue;
})
The first part is calculating the default value for the given destination value, based on the type of the destination value. The latter part is checking if the destination already have a non-default value or not. Consecutive calls of Map() with the same target object will fill the "missing" property values until all the property values are set (in this example). If necessary, check the srcMember value and it's default type as well to fine-tune when the value should be copied or not.
I am working on a project , in order to use the Identity features in the last steps, I had to make some changes on database and add Identity to it. Also, inherit DatabaContext from IdentityDbContext. In my previous method I used to make a new object of DatabaseContext and work on it :
private DatabaseContext db = new DatabaseContext();
But with the new changes, when I pass the model to View or apply a conditional input, it returns a null value, for example, the following code:
public JsonResult CheckUserNameAvailability(Int32 UserCode)
{
var SearchData = db.Persons.Where(p => p.Pcode == UserCode).FirstOrDefault();////this line
if (SearchData != null)
{
TempData["code"] = 0;
return Json(1);
}
else
{
TempData["code"] = UserCode;
return Json(0);
}
}
On the line
var SearchData = db.Persons.Where (p => p.Pcode == UserCode) .FirstOrDefault();
return Error
System.Data.SqlTypes.SqlNullValueException: 'Data is Null. This method or property cannot be called on Null values.
even though the database is not empty and this code used to work properly. If someone could provide some assistance, it would be terrific!
and This is Model:
namespace LeaveSheet.Models
{
public class Person
{
[Key]
public Int32 Id { get; set; }
public Int32 Pcode { get; set; }
public string Name { get; set; }
public string Family { get; set; }
}
}
codes in startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(p => p.UseSqlServer(#"Data Source =. ; Initial Catalog = LeaveSheet; Integrated Security=True;MultipleActiveResultSets=true "));
services.AddControllersWithViews();
services.AddIdentity<User, Role>().AddEntityFrameworkStores<DatabaseContext>().AddDefaultTokenProviders();
}
Check whether the ID column in person table has always a value.
Even though it is the key column in model, check whether it is same in DB as well.
I try example in this topic
https://github.com/DevExpress-Examples/XPO_how-to-create-an-xpclassinfo-descendant-to-dynamically-build-a-persistent-class-structure-e1729
But It only works for single primary key tables. So i searched more Found this:
https://github.com/DevExpress-Examples/XPO_how-to-create-persistent-classes-mapped-to-tables-with-a-composite-primary-key-at-runtime-e4606
Buts there's big different and I think its not satisfy because it speaking about composite key( one Key which have many columns)
So all I need please an example of creating XPO dynamically from SQL -Server Table:
My Table Schema as following
The XPOCollectionSource then binding to grid in server-mode… Thats all I need.
Code I Use
XPServerCollectionSource GetServerModeSourceForTable(IDbConnection connection, string tableName) {
XPDictionary dict = new ReflectionDictionary();
XPClassInfo classInfo = dict.CreateClass(dict.QueryClassInfo(typeof(LiteDataObject)),
tableName);
DBTable[] tables = ((ConnectionProviderSql)XpoDefault.GetConnectionProvider(connection,
AutoCreateOption.None)).GetStorageTables(tableName);
foreach (DBColumn col in tables[0].Columns) {
XPMemberInfo member = classInfo.CreateMember(col.Name, DBColumn.GetType(
col.ColumnType));
if (tables[0].PrimaryKey.Columns.Contains(col.Name))
member.AddAttribute(new KeyAttribute());
}
return new XPServerCollectionSource(new Session(XpoDefault.GetDataLayer(
connection, dict, AutoCreateOption.None)), classInfo);
}
At a glance. How to use XPServerCollectionSource with Dynamically created XPO object. with two primary keys.
https://github.com/DevExpress-Examples/XPO_how-to-create-persistent-classes-mapped-to-tables-with-a-composite-primary-key-at-runtime-e4606/blob/19.2.7%2B/CS/XpoConsoleApplication/XPComplexCustomMemberInfo.cs
This is a class must used. who need more details about that. I can help him for free.
Implementation can be like:
[NonPersistent]
public class XPDynamicObject : XPLiteObject
{
public XPDynamicObject(Session session) : base(session) {}
public XPDynamicObject(Session session, XPClassInfo classInfo) : base(session, classInfo) { }
}
Button_Click or any event :
XPDynamicObject.AutoSaveOnEndEdit = false;
ReflectionDictionary dic = new ReflectionDictionary();
var classInfo = dic.CreateClass(dic.GetClassInfo(typeof(XPDynamicObject)), "general.users");
// WE MUST get schema from database .... via ConnectionProviderSql this is only way
var provider = XpoDefault.GetConnectionProvider(MSSqlConnectionProvider.GetConnectionString("(local)", "testdb"), AutoCreateOption.None) as ConnectionProviderSql;
// Composite Key - this is only way to add composite key dynamically
XPComplexCustomMemberInfo user_key = new
XPComplexCustomMemberInfo(classInfo, "user_key", typeof(object), new KeyAttribute(), new PersistentAttribute(), new BrowsableAttribute(false));
user_key.AddSubMember("user_brn", typeof(int), new PersistentAttribute("user_brn"));
user_key.AddSubMember("user_num", typeof(int), new PersistentAttribute("user_num"));
var user_name = classInfo.CreateMember("user_name", typeof(string));
user_name.AddAttribute(new PersistentAttribute("user_name"));
dal = new SimpleDataLayer(dic, provider);
XPServerCollectionSource xpServerCollectionSource = new XPServerCollectionSource(session, classInfo); // XPServerCollectionSource Only Editable server-mode datasource via ALlowNew, AllowEdit, AllowRemove properties
GridControl.DataSource = xpServerCollectionSource ;
Full Cheat Sheet (Design-Time and Run-Time XPOs)
For Searching for Object by Key or Condition use following:
Edit: 2021-05-24
For those who asks about Associations with composite keys. Its really nightmare. By default Xpo doesn't provide that. instead you can use a Session.GetObjectByKey() in child-class and a new XPCollection in parent class. But actually its something little slowly that native way with an artificial key.
At a glance: Use Artifcial / Single Key Column for Better any ORM
compatibility. Otherwise It will be nightmare in database later. If
you for example support another or replace current ORM.
Here's a code for associations with composite keys. (Note that you need to convert it dynamically like above pictures. You maybe need to create custom XPMemberInfo and override GetValue() to return Session.GetObjectByKey() and create new XPCollection. (really bad stuff to do and worst ever and non-supported ever by Xpo. So Composite-Keys technically are bad way except if you need create Single Table bind it to grid. EX:- SalesOrderDetail table which doesn't have more child tables)
[Persistent("users")]
public class user : XPLiteObject
{
public user(Session session) : base(session) { }
public user(Session session, XPClassInfo classInfo) : base(session, classInfo) { }
[Key, Persistent]
public user_key user_key { get; set; }
[Persistent("user_name")]
public string user_name { get; set; }
private XPCollection<Trans> _trans;
public XPCollection<Trans> trans
{
get
{
if (_trans == null)
_trans = new XPCollection<Trans>(Session, CriteriaOperator.Parse($"user_brn = {user_key.user_brn} AND user_num = {user_key.user_num}"));
return _trans;
}
}
}
public class Trans : XPLiteObject
{
public Trans(Session session) : base(session) { }
public Trans(Session session, XPClassInfo classInfo) : base(session, classInfo) { }
[Key,Persistent("TransID")]
public int TransID { get; set; }
public int user_brn { get; set; }
public int user_num { get; set; }
public user user
{
get
{
var key = new user_key();
key.user_brn = 1;
key.user_num = 3;
var obj = Session.GetObjectByKey<user>(key, true);
return obj;
}
}
}
// Composite-Key
public struct user_key
{
[Persistent("user_brn")]
public int user_brn { get; set; }
[Persistent("user_num")]
public int user_num { get; set; }
}
A Dynamic run-time version from above model:
class XPCompositeAssociationMemberInfo : XPCustomMemberInfo
{
public XPCompositeAssociationMemberInfo(XPClassInfo owner, string propertyName, Type propertyType, XPClassInfo referenceType, bool nonPersistent, bool nonPublic) : base(owner, propertyName, propertyType, referenceType, nonPersistent, nonPublic)
{
}
public override object GetValue(object theObject)
{
XPDynamicObject val = theObject as XPDynamicObject;
if(val != null)
{
var user_num = val.GetMemberValue("user_num");
IdList vals = new IdList();
vals.Add(1);
vals.Add(user_num);
return val.Session.GetObjectByKey(ReferenceType, vals);
}
return val;
}
}
XPDynamicObject.AutoSaveOnEndEdit = false;
ReflectionDictionary dic = new ReflectionDictionary();
var userClassInfo = dic.CreateClass(dic.GetClassInfo(typeof(XPDynamicObject)), "users");
// WE MUST get schema from database .... via ConnectionProviderSql this is only way
var provider = XpoDefault.GetConnectionProvider(MSSqlConnectionProvider.GetConnectionString("(local)", "testdb"), AutoCreateOption.None) as ConnectionProviderSql;
//DBTable Table = provider.GetStorageTables("users")[0];
// Composite Key - this is only way to add composite key dynamically
XPComplexCustomMemberInfo user_key = new XPComplexCustomMemberInfo(userClassInfo, "user_key", typeof(object), new KeyAttribute(), new PersistentAttribute(), new BrowsableAttribute(false));
user_key.AddSubMember("user_brn", typeof(int), new PersistentAttribute("user_brn"));
user_key.AddSubMember("user_num", typeof(int), new PersistentAttribute("user_num"));
var user_name = userClassInfo.CreateMember("user_name", typeof(string));
user_name.AddAttribute(new PersistentAttribute("user_name"));
var transClassInfo = dic.CreateClass(dic.GetClassInfo(typeof(XPDynamicObject)), "Trans");
var TransID = transClassInfo.CreateMember("TransID", typeof(int), new KeyAttribute());
var user_brn = transClassInfo.CreateMember("user_brn", typeof(int));
var user_num = transClassInfo.CreateMember("user_num", typeof(int));
XPCompositeAssociationMemberInfo userMI = new XPCompositeAssociationMemberInfo(transClassInfo, "user", typeof(object), userClassInfo, true, false);
dal = new SimpleDataLayer(dic, provider);
Session session = new Session(dal);
XPServerCollectionSource xpServerCollectionSource = new XPServerCollectionSource(session, transClassInfo); // XPServerCollectionSource Only Editable server-mode datasource via ALlowNew, AllowEdit, AllowRemove properties
xpServerCollectionSource.DisplayableProperties = "TransID;user.user_key.user_num;user.user_name";
Finally, IMPORTANT !
Its really very very hard to achieve Composite-Keys cases at runtime
in any Xpo. Instead replace with Single Column Key(Identity or
Artificial one. GUID one etc.)
I don't know if this is what you want, but I'll share my code using XPInstantFeedbackSource.
You can get more information at How To Implement The Instant Feedback Mode Xpo
XPLiteObject LogXPOModel
[Persistent("log.t_log")]
public class LogXPOModel : XPLiteObject
{
[Key, DevExpress.Xpo.DisplayName("id")]
public long id { get; set; }
[Key, DevExpress.Xpo.DisplayName("recv_time")]
public long recv_time
...
}
Window MainWindow
public class MainWindow : Window
{
public XPInstantFeedbackSource SqliteXPInstantFeedbackSource
public MainWindow() {
...
SqliteXPInstantFeedbackSource = new XPInstantFeedbackSource();
SqliteXPInstantFeedbackSource.ObjectType = typeof(LogXPOModel);
SqliteXPInstantFeedbackSource.ResolveSession += SqliteXPInstantFeedbackSource_ResolveSession;
SqliteXPInstantFeedbackSource.DismissSession += SqliteXPInstantFeedbackSource_DismissSession;
View.Grid.ItemsSource = SqliteXPInstantFeedbackSource;
}
...
}
Like that — my table has two keys; and, it does not incur any problems.
I am learning entity framework core, using it with SQLITE
I have 2 tables in the database.
Messages:
CREATE TABLE `Messages` (
`Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`IsDeleted` INTEGER NOT NULL,
`FromUserId` INTEGER,
`ToUserId` INTEGER,
`SendDate` TEXT NOT NULL,
`ReadDate` TEXT NOT NULL,
`MessageContent` TEXT,
CONSTRAINT `FK_Messages_Users_ToUserId` FOREIGN KEY(`ToUserId`) REFERENCES `Users`(`Id`) ON DELETE RESTRICT,
CONSTRAINT `FK_Messages_Users_FromUserId` FOREIGN KEY(`FromUserId`) REFERENCES `Users`(`Id`) ON DELETE RESTRICT
);
and Users table:
CREATE TABLE `Users` (
`Id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`IsDeleted` INTEGER NOT NULL,
`Name` TEXT,
`DisplayName` TEXT,
`Gender` INTEGER NOT NULL,
`BirthDate` TEXT NOT NULL
);
my c# classes look like:
public class User
{
public int Id {get;set;}
public bool IsDeleted{get;set;}
[Required]
public string Name{get;set;}
public string DisplayName{get;set;}
[Required]
public char Gender{get;set;}
[Required]
public DateTime BirthDate{get;set;}
public User(string n, string dn, char g, DateTime bd)
{
Name=n; DisplayName = dn; Gender = g; BirthDate = bd;
}
protected User(){}
public override string ToString()
{
return string.Format("{0} ({1}) {2}", this.Name, this.Gender,this.BirthDate.ToShortDateString());
}
}
and Messages class:
public class Message
{
public int Id{get;set;}
public bool IsDeleted{get;set;}
[Required]
public Users.User FromUser{get;set;}
[Required]
public Users.User ToUser{get;set;}
[Required]
public DateTime SendDate{get;set;}
public DateTime? ReadDate{get;set;}
[Required]
public string MessageContent{get;set;}
protected Message(){}
public Message(User from, User to, string content)
{
this.FromUser = from;
this.ToUser = to;
this.MessageContent = content;
this.SendDate = DateTime.Now;
this.ReadDate = DateTime.Now;
}
}
users table works, but when I try to add a new entity to the messages by this:
public override int Insert(Message entity)
{
dbc.Messages.Add(entity);
dbc.SaveChanges();
return entity.Id;
}
I get the following error:
SQLite Error 19: 'UNIQUE constraint failed
I have no idea what is wrong. When I manually inserted data to a database (using DB browser for sqlite) it works.
Are the relationships in the classes ok?
My dbContextSnapshot is:
[DbContext(typeof(RandevouDbContext))]
partial class RandevouDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
modelBuilder.Entity("RandevouData.Messages.Message", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("FromUserId");
b.Property<bool>("IsDeleted");
b.Property<string>("MessageContent")
.IsRequired();
b.Property<DateTime?>("ReadDate");
b.Property<DateTime>("SendDate");
b.Property<int>("ToUserId");
b.HasKey("Id");
b.HasIndex("FromUserId");
b.HasIndex("ToUserId");
b.ToTable("Messages");
});
modelBuilder.Entity("RandevouData.Users.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("BirthDate");
b.Property<string>("DisplayName");
b.Property<char>("Gender");
b.Property<bool>("IsDeleted");
b.Property<string>("Name")
.IsRequired();
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("RandevouData.Messages.Message", b =>
{
b.HasOne("RandevouData.Users.User", "FromUser")
.WithMany()
.HasForeignKey("FromUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("RandevouData.Users.User", "ToUser")
.WithMany()
.HasForeignKey("ToUserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
By the way. I cannot configure mapping from two sides.
The Message entity has UserFrom and UserTo fields,
but the User entity can not have Messages, because one time he is "userFrom" and other time he can be "userTo".
And there is also a code, where i create Message entity
public int SendMessage(int senderId, int receiverId, string content)
{
var dao = new MessagesDao();
var usersDao = new UsersDao();
var userService = new UserService(mapper);
var sender = usersDao.Get(senderId);
var receiver = usersDao.Get(receiverId);
var entity = new Message(sender,receiver,content);
var id = dao.Insert(entity);
return id;
}
user dao where i override GET method
public override User Get(int id)
{
using (var context = new RandevouDbContext())
{
var user = dbc.Find<User>(id);
return user;
}
}
and MessagesDao where i override Add method
public override int Insert(Message entity)
{
using (var dc = new RandevouDbContext())
{
dc.Messages.Add(entity);
dc.SaveChanges();
return entity.Id;
}
}
btw, i dont know if it is ok, but i have 0 entries to update (!?)
To fix 'UNIQUE' constraint exceptions you need to make sure all the entities you're working with are being tracked by the context you want to save to.
e.g.
// Get user from one context
User thisUser = db.Users.Find(3);
//Now imagine you have another context which has been created later on
DbContext newContext = new DbContext();
//and you want to create a message, assign it to your user
//and save it to the new context
Message newMessage = new Message
{
Text = "Hello",
User = thisUser
}
newContext.Messages.Add(newMessage);
// saving this to newContext will fail because newContext is not tracking thisUser,
// Therefore attach it...
newContext.Users.Attach(thisUser).
//now newContext is tracking thisUser *AND* newMessage, so the save operation will work
newContext.SaveChanges();
Ok, i solved problems.
Read some articles about EF DbContext.
When i have used two other context's for GET User and CREATE message,
the entities (user1 and user2) were not the same entities which i have used for second context. I mean, the entiies were this same, but they had another tracking
EF Core / Sqlite one-to-many relationship failing on Unique Index Constraint
So i solved problem by using one context on one Business-Transaction (not one database transaction). Just moved creating context to business logic, and the Generic dao class was waiting for context in constructor, like this:
public class MessagesDao : BasicDao<Message>
{
public MessagesDao(RandevouDbContext dbc) : base(dbc)
{
}
public IQueryable<Message> QueryMessages()
{
return dbc.Messages.AsQueryable();
}
public override int Insert(Message entity)
{
dbc.Messages.Add(entity);
dbc.SaveChanges();
return entity.Id;
}
}
public abstract class BasicDao<TEntity> where TEntity : BasicRandevouObject
{
protected RandevouDbContext dbc;
public BasicDao(RandevouDbContext dbc) { this.dbc = dbc; }
public virtual int Insert(TEntity entity)
{
dbc.Add<TEntity>(entity);
dbc.SaveChanges();
return entity.Id;
}
public virtual void Update(TEntity entity)
{
dbc.Update(entity);
dbc.SaveChanges();
}
public virtual void Delete(TEntity entity)
{
dbc.Remove(entity);
dbc.SaveChanges();
}
public virtual TEntity Get(int id)
{
var entity = dbc.Find<TEntity>(id);
return entity;
}
}
and in business-logic code :
public int SendMessage(int senderId, int receiverId, string content)
{
using (var dbc = new RandevouDbContext())
{
var dao = new MessagesDao(dbc);
var usersDao = new UsersDao(dbc);
var userService = new UserService(mapper);
var sender = usersDao.Get(senderId);
var receiver = usersDao.Get(receiverId);
var entity = new Message(sender,receiver,content);
var id = dao.Insert(entity);
return id;
}
}
now everything works. By the way it is still dirty code, because i am just trying some things, but i hope if someone will have similiar problem then he will find this post.
What's missing from your (very detailed) question is the makeup of the Message you're passing to Insert(). Have you previously saved the two Users to the database, or are they two new objects, too? If they are new then their ID should be zero so that EF knows to create them first, prior to creating the Message.
i have a problem with Nhibernate collection mapped asSet, in few word when i add new item to collection all other item are lost.
but what is more strange is that those appens only once, every following insert give no problem since i restart app
this my object:
public class Attivita : BaseObject<Attivita, int> {
public override int Id { get; set; }
private ICollection<SAL> _statiAvanzamentoLavori = new List<SAL>();
public virtual IEnumerable<SAL> StatiAvanzamentoLavori {
get { return _statiAvanzamentoLavori.ToArray(); }
}
public virtual void AddSAL(DateTime dataRiferimento, DateTime dataAvvioPrevisto, DateTime dataConclusionePrevista) {
if (!(_statiAvanzamentoLavori.Any(x => x.DataRiferimento == dataRiferimento))){
SAL sal = new SAL{
DataRiferimento = dataRiferimento,
DataAvvioPrevisto = dataAvvioPrevisto,
DataConclusionePrevista = dataConclusionePrevista,
};
_statiAvanzamentoLavori.Add(sal);
}
}
}
public class SAL : EquatableObject<SAL>
{
protected internal virtual int id {get; set;}
public virtual DateTime DataRiferimento {get; set;}
public virtual DateTime DataAvvioPrevisto {get; set;}
public virtual DateTime DataConclusionePrevista {get; set;}
}
BaseObject override Equals using ID property
EquatableObject override Equals comparing all object property
the maps:
public AttivitaMap()
{
Table("Attivita_T041");
Id(x => x.Id)
.Column("Kint_T041_IdAttivita").GeneratedBy.Assigned();
HasMany<SAL>(x => x.StatiAvanzamentoLavori)
.Access.CamelCaseField(Prefix.Underscore)
.KeyColumns.Add("int_T018_IdAttivita", x => x.UniqueKey("IX18_1"))
.ForeignKeyConstraintName("FK18_IdAtt") .Not.LazyLoad().Cascade.All();
}
}
public SALMap()
{
Table("SAL_T018");
Id(x => x.id)
.Column("Kint_T018_IdSAL")
.GeneratedBy.Identity();
Map(x => x.DataRiferimento).UniqueKey("IX18_1")
.Not.Nullable();
Map(x => x.DataAvvioPrevisto);
Map(x => x.DataConclusionePrevista).Not.Nullable();
}
and finally the test:
[Test]
public void AttivitaRepositoryCanInsertSALOnNotEmptyCollection() {
var attivita = fixture.Create<Attivita>();
var SAL1 = fixture.Create<SAL>();
var SAL2 = fixture.Create<SAL>();
attivita.AddSAL(SAL1.DataRiferimento, SAL1.DataAvvioPrevisto, SAL1.DataConclusionePrevista);
using (UnitOfWork uow = GetUnitOfWork()) {
uow.Attivita.Update(attivita);
uow._session.Transaction.Commit();
}
using (UnitOfWork uow = GetUnitOfWork()) {
attivita = uow._session.Get<Attivita>(attivita.Id);
// this test pass
attivita.StatiAvanzamentoLavori.Count().Should().Be(1);
attivita.AddSAL(SAL2.DataRiferimento, SAL2.DataAvvioPrevisto, SAL2.DataConclusionePrevista, SAL2.StatoAvanzamentoAttivita);
uow.Attivita.Update(attivita);
uow._session.Transaction.Commit();
}
using (UnitOfWork uow = GetUnitOfWork()) {
// this test fails: expected 2 found 1
uow._session.Get<Attivita>(attivita.Id).StatiAvanzamentoLavori.Count().Should().Be(2);
var SAL3 = fixture.Build<SAL>().With(x => x.StatoAvanzamentoAttivita, statoAvanzamentoTest).Create();
using (UnitOfWork uow = GetUnitOfWork()) {
attivita = uow._session.Get<Attivita>(attivita.Id);
attivita.AddSAL(SAL3.DataRiferimento, SAL3.DataAvvioPrevisto, SAL3.DataConclusionePrevista, SAL3.StatoAvanzamentoAttivita);
uow.Attivita.Update(attivita);
uow._session.Transaction.Commit();
}
using (UnitOfWork uow = GetUnitOfWork()) {
uow._session.Get<Attivita>(attivita.Id).StatiAvanzamentoLavori.Count().Should().Be(3);
// this test fails: expected 3 found 2
}
}
}
so when i add the second sal the first is lost but when i add the third the second remain, why??
looking at SQL log i see that when i add the second SAL un update is fired to SAL TABLE settings IdAttivita to NULL, so link to Attivita is lost
i read here that removing and inserting all links is behaviour is by design with Bag collection but i have SET with primary key
More strange if i change AsSet to AsBag (or simply remove AsAet) for collection mapping all works fine, but i need Set for other things (like multiple fetch)
As answered by Nhibernate developer on their bugtracker problem was mixing Equals method: Value object must be compared by their properties and entity by their id
My fault was using peropery comparison with the entity SAL
So i swtich it to use BaseObject and all works fine