ict.ken.be

 

Posts in Category: EF

Entity Framework in the Enterprise - Notes 

Categories: EF Notes

by Julie Lerman

UI - Service Layer - Bus Layer - Repository/Unit of Work - Data Layer - Database

EF should stay in the data layer.
Split your context in smaller bounded contexts.

Bounded DbContext

  • Inspired on "Domain-Driven Design by Eric Evans" (domaindrivendesign.org)
  • All your navigation properties get pulled into the model.
  • Transition between context by id or by object.
  • Entity with property that is not persisted in the database: public decimal LineTotal { get; set; } ... In map class add Ignore(t => t.LineTotal)
  • Do not include navigation in the context: In OnModelCreating add modelBuilder.Ignore<T>();
  • Make sure non related is included: In OnModelCreating add modelBuilder.Entity<T>();
  • Migrations and creations: Add one model that includes all of your tables and use this one.
  • Put each context in it's own project.
  • Put the mappings also in their separated project. eg. BaseDataLayer - CompanyDatabaseInitializers - CustomerServiceBoundedContext - DataLayerMappings - DomainClasses - ReturnsBoundedContext - SalesBoundedContext
  • Use EF powertools to visualize your context and see what will be pulled in.
  • And then cherry pick what you need (eg. probably do not need the relationships)

Repositories and Unit of Work

  • Encapsulate Redundant Data Access Code
  • GetCustomerById(cid), GetAllCustomers, GetCurrentCustomers, GetLocalCustomers, GetFilteredCustomers
  • RemoveCustomer, AddCustomer

Install-Package entityframework
Install-Package t4scaffolding

scaffold repository DomainClasses.Customer -DbContextType:SalesContext

Always use ctx.Customers.Find(id) cause it will not roundtrip when not needed.

DbSet.Add(root) - All elements State=Added - Insert for each element, even if pre-existing in db
DbSet.Attach(root) - All elements State=Unchanged - Any new records with missing FKs will throw
Context.Entry(root).State=EntityState - Context attaches full graph, all entities State=Unchanged, Root (only) set to specific state - Any new with missing FKs will throw

Disconnected Graph
Attaching to Context: all new -> DbSet.Add(customer)
Root new, existing unchanged: Entry(customer).State=Added
Editing: Entry(customer).State=Modified then Entry(*each item*).State=Modified
Root edited and added: DbSet.Add(customer) then Entry(*each non added*).State=*correct state*

Make objects track their own state:
IObjectWithState
enum State { Added, Unchanged, Modified, Deleted }

public abstract class Person:IObjectWithState
[NotMapped]
public State State { get; set;}

Repository depends on the value of client-side State
Developer using repository MUST set client-side State!

StateHelpers - EntityState ConvertState(State state)
ContextHelpers - ApplyStateChanges(this DbContext context)

Difference between long running and short running context (or implement two repos)

Let repositories share the same context (Unit of work)
Move save method to unit of work
CustomerRepository contains Unit of work that contains a context

ReferencesContext
Especially when reading data: _context.Customers.AsNoTracking();

A DbSet represents the collection of all entities in the context, or that can be queried from the database, of a given type.
DbSet object are created from a DbContext using the DbContext.Set method
A DbContext instance represents a combination of the unit of work and repository patterns such that it can be used to query a database and group together changes that will then be written back to the store as a unit. DbContext is conceptually similar to ObjectContext.

  • Why build a repository on top op EF? EF is a repository!
  • Should you return an IQueryable from a repository
  • How much abstraction and refactoring? There will always be more.

Automated Testing Overview

  • Unit Testing
  • Integration Testing
  • Interaction Testing
  • System Testing
  • User Interface Testing

1. Complete model with Seed method
2. Initialize Database for each integration test
3. Use a testing database that can get destroyed frequently

Test your data annotations: test for DbEntityValidationException

Fake Context & Fake Data
IDbSet<TEntity> : DbQuery<TEntity>, IDbSet<TEntity>, IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable where TEntity : class

(you can also use IObjectSet)
public abstract class MyFakeDbSet<T> : IDbSet<T> where T : class, new()
{
}

Create implementation to override the find method

IUoW needs IContext

SalesPromotionAccessor (to hide the context and wrap it with your unit of work)

Void System.Data.Objects.ObjectContextOptions.set_UseConsistentNullReferenceBehavior(Boolean) 

Categories: EF
  • Method not found: 'Void System.Data.Objects.ObjectContextOptions.set_UseConsistentNullReferenceBehavior(Boolean)'

You probably still need to install .Net 4.5 because you are using Entity Framework for this version.

After installing .Net 4.5 you might need a aspnet_regiis -i or not...

See 'EntityValidationErrors' while debuging 

Categories: EF

For the lazy ones that did not put a try-catch:

Press ctrl+alt+q and re-evalute this expression.

((System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors

For the less lazy ones:

catch (DbEntityValidationException dbEx)
{
    foreach (var validationErrors in dbEx.EntityValidationErrors)
    {
       Trace.TraceInformation("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:", validationErrors.Entry.Entity.GetType().Name, validationErrors.Entry.State); foreach (var validationError in validationErrors.ValidationErrors)
        {
            Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
        }
    }
}

And if you don't like exceptions:

var validationErrors = model.GetValidationErrors();
var h = validationErrors.SelectMany(
x => x.ValidationErrors.Select(
f => "Entity: " +(x.Entry.Entity) + " : " + f.PropertyName + "->" + f.ErrorMessage
));

Code First 

Categories: EF

O'Reilly's Programming Entity Framework - Code First

By Julia Lerman & Rowan Miller

  • Table name will be plural of class name created with dbo schema.
  • Column names same as properties they map to.
  • Strings are mapped to nvarchar(max)
  • Byte[] is mapped to varbinary(max)
  • Bool to bit not null
  • A class must have a key property (default named Id or typenameId)
  • One-to-Many Relationships with foreign key [Name of navigation property]_[Primary key of related class]
  • Foreign keys are nullable by convention.
  • In memory metadata created from code or EDMX model (System.Data.Metadata.Edm)
  • Only use DetectChanges if needed for performance tuning.

Data Annotations (System.ComponentModel.DataAnnotations) vs Fluent API (modelBuilder.Entity<AnimalType>())

[Table("Species")]
this.ToTable("Species");

[Table("Species", Schema="baga")]
this.ToTable("Species", "baga");

[Column("LocationName")]
this.Property(d => d.Nam).HasColumnName("LocationName");

[Key]
this.HasKey(t => t.Guid);

[Required]
this.Property(p => p.TypeName).IsRequired();

[MinLength(10), MaxLength(500)]
this.Property(p => p.Description).HasMaxLength(500); (no min available)

[Column(TypeName="image")]
this.Property(d => d.Photo).HasColumnType("image");

[DatabaseGeneratedOption.Identity]
this.Property(t => t.Guid).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

[Timestamp] must be a byte[]
this.Property(t => t.TimeStamp).IsRowVersion();

[ConcurrencyCheck]
this.Property(t => t.SocialSecurityNumber).IsConcurrencyToken();

[ComplexType]
modelBuilder.ComplexType<Address>();

[ForeignKey("BillingAddressId")]
modelBuilder.Entity<User>().HasRequired(a => a.BillingAddress).WithMany().HasForeignKey(u => u.BillingAddressId);

[Required]
modelBuilder.Entity<PersonPhoto>().HasRequired(p => p.PhotoOf).WithRequiredDependent(p => p.Photo);

[InverseProperty("PrimaryContactFor")]
modelBuilder.Entity<Lodging>().HasOptional(l => l.PrimaryContact).WithMany(p => p.PrimaryContactFor);

this.Property(t => t.SomeAscii).IsUnicode(false);

this.Property(t => t.SomeMiles).HasPrecision(8,1);

foo.WillCascadeOnDelete(false);

[NotMapped]
Ignore(d => d.TodayForecast);
  • By default, Code First will create the database only if it doesn't already exist.
  • Complex types have no key property, can only contain primitive properties and when used as a property in another class, the property must represent a single instance. It cannot be a collection type. [HostPropertyName_PropertyName]
  • Entity.Has[Multiplicity](Property).With[Multiplicity](Property)
  • .HasOptional, .HasRequired, .HasMany + WithOptional, WithRequired, WithMany
  • [Target Type Key Name], [Target Type Name] + [Target Type Key Name], [Navigation Property Name] + [Target Type Key Name]
var tripWithActivities = context.Trips.Include("Activities").FirstOrDefault();

Table splitting
modelBuilder.Entity<Person>().ToTable("People");
modelBuilder.Entity<PersonPhoto>().ToTable("People");

Entity splitting
Map(m => { m.Properties(d => new { d.Name, d.Country, d.Description }); m.ToTable("Locations"); });
Map(m => { m.Properties(d => new { d.Photo }); m.ToTable("LocationPhotos"); });
  • For protected and private properties, the configuration class must be nested inside the class that is part of the model.
  • The setter can be marked with a more restrictive accessor, but the getter must remain public for the property to be mapped automatically. (not supported in medium trust because of reflection)
public class Person
{
public int PersonId { get; set; }
private string Name { get; set; }

public class PersonConfig : EntityTypeConfiguration<Person>
{
public PersonConfig()
{
Property(b => b.Name);
}
}

public string GetName()
{
return this.Name;
}

public static Person CreatePerson(string name)
{
return new Person { Name = name };
}
}
modelbuilder.Configuration.Add(new Person.PersonConfig());

public string FullName
{
get { return String.Format("{0}{1}",FirstName.Trim(), LastName); }
}

Table per hierarchy (TPH) describes mapping inherited types to a single database table that uses a discriminator column to differentiate one subtype from another. This is default for Entity Framework. (nvarchar(128), not null containing the type name)

  • Map(m => { m.ToTable("Lodgings"); m.Requires("LodgingType").HasValue("Standard"); });
  • Map<Resort>(m => { m.Requires("LodgingType").HasValue("Resort"); });
  • Map(m => { m.ToTable("Lodgings"); m.Requires("IsResort").HasValue(false); });
  • Map<Resort>(m => { m.Requires("IsResort").HasValue(true); });

Table per type (TPT) only stores properties for base class in a single table. Additional properties are stored in separate table.

  • modelBuilder.Entity<Resort>().ToTable("Resorts");
  • modelBuilder.Entity<Lodging>().Map<Resort>(m => { m.ToTable("Resorts"); });

Table per concrete type (TPC) stores all the properties for each type in a separate table. Map inheritance to tables with common overlapping fields.

  • modelBuilder.Entity<Lodging>().Map(m => { m.Table("Lodgings"); }).Map<Resort>(m => { m.ToTable("Resorts"); m.MapInheritedProperties(); } );

Mapping foreign keys

  • HasRequired(l => l.Destination).WithMany(d => d.Lodgings).Map(c => c.MapKey("destination_id"));
  • HasRequired(l => l.Destination).WithMany(d => d.Lodgings).Map(c => c.MapKey("destination_id").ToTable("LodgingInfo"));
  • HasMany(t => t.Activities).WithMany(a => a.Trips).Map(c => c.ToTable("TripActivities"));
  • HasMany(t => t.Activities).WithMany(a => a.Trips).Map(c => { c.ToTable("TripActivities"); c.MapLeftKey("TripIdentifier"); c.MapRightKey("ActivityId"); });
  • MapLeftKey affects the foreign key column that points to the class being configured!

Connections

// you need to take care of disposing the connection unless your context owns the connection (property)
using (var connection = new SqlConnection(connectionString))
{
using (var db = new MyDbContext(connection)) { ... }
using (var dbUsingSameConnection = new MyDbContext(connection)) { ... }
}
  • SqlConnectionFactory : IDbConnectionFactory
  • Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");

Initialization is triggered the first time that the context is used. Initialization occurs lazily.

  • Database.SetInitializer(new DropCreateDatabaseIfModelChanges<BreakAwayContext>());
  • Database.SetInitializer(new CreateDatabaseIfNotExists<BreakAwayContext>());
  • Database.SetInitializer(new DropCreateDatabaseAlways<BreakAwayContext>()); // use system.Transactions.TransactionScope if you have a lot of integration tests
  • EFCodeFirst.CreateTablesOnly nuget package
using (var context = new BreakAwayContext())
{
try
{
context.Database.Initialize(force: false);
}
catch (Exception ex) { ... }
}
Database.SetInitializer(null); // do not magically create a database
<appSettings>
<add key="DatabaseInitializerForType DataAccess.BreakAwayContext, DataAccess" value="Disabled" />
</appSettings>

public class PromptForDropCreateDatabaseWhenModelChanges<TContext> : IDatabaseInitializer<TContext> where TContext : DbContext
{
public void InitializeDatabase(TContext context)
{
var exists = context.Database.Exists();
if (exists && context.Database.CompatibleWithModel(true)) return;
if (exists)
{
// prompt
context.Database.Delete();
}
context.Database.Create();
}
}

public class DropCreateBreakAwayWithSeedData : DropCreateDatabaseAlways<BreakAwayContext>
{
protected override void Seed(BreakAwayContext context)
{
context.Database.ExecuteSqlCommand("CREATE INDEX IX_Lodgings_Name ON Lodgings (Name)");
context.Destinations.Add(new Destination { Name = "foo" });
base.Seed(context);
}
}

Mapping to Updatable views

Using views to populate objects

var destinations = context.Destinations.SqlQuery(@"SELECT Id AS DestinationId, Name FROM dbo.TopTenDestinations");

public class DestinationSummary
{
public int DestinationId { get; set; }
public string Name { get; set; }
public int ResortCount { get; set; }
}
var summary = context.Database.SqlQuery<DestinationSummary>("SELECT * FROM dbo.DestinationSummaryView");

Stored Procedures

var country = "Belgium";
var keywords ="sun, yeahright";
var destinations = context.Database.SqlQuery<DestinationSummary>("dbo.GetDestinationSummary @p0, @p1", country, keywords);

Removing Conventions

  • modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();

Model Caching

  • Using the same DbCompiledModel to create contexts against different types of database servers is not supported. Instead, create a separate DbCompiledModel for each type of server being used.

var sql_model = GetBuilder().Build(new DbProviderInfo("System.Data.SqlClient", "2008")).Compile();
var context = new BreakAwayContext (connection, sql_model);
public static DbModelBuilder GetBuilder()
{
var builder = new DbModelBuilder();
builder.Entity<EdmMetadata>().ToTable("EdmMetadata");
...
builder.Entity<Trip>();
return builder;
}

EdmMetadata Table

  • Snapshot of your model as a SHA256 hash.
  • var modelHash = EdmMetadata.TryGetModelHash(context);
  • var databaseHash = context.Set<EdmMetadata>().Single().ModelHash;
  • var compatible = context.Database.CompatibleWithModel(throwIfNoMetadata:true);
  • modelBuilder.Conventions.Remove<IncludeMetadataConvention>();

More

Multiple objects sets per type are not supported 

Categories: EF

You copy and pasted a DbSet and then you forget to rename the type...

This error occurs if your DbContext class exposes multiple DbSet<T> properties where T occurs more than once. Entitfy framework is not able to figure out which DbSet an instance of type T belongs to.

public DbSet<Course> Courses { get; set; }
public DbSet<Course> Chapters { get; set; }
Page 1 of 2 1 2 > >>