EfCore 3 and Owned Type in same table, How do you set owned instance - entity-framework-core

How do you set owned type instance with efcore3?
In following example an exception is raised
'The entity of type 'Owned' is sharing the table 'Principals' with
entities of type 'Principal', but there is no entity of this type with
the same key value that has been marked as 'Added'.
If I set Child property inline savechanges doesn't update child properties
I can't find any example about this. I tried with several efcore3 builds and daily builds. What didn't I understand?
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace TestEF
{
class Program
{
static void Main(string[] args)
{
var id = Guid.NewGuid();
using (var db = new Ctx())
{
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var p = new Principal {Id = id};
db.Principals.Add(p);
db.SaveChanges();
}
using (var db = new Ctx())
{
var p = db.Principals.Single(o => o.Id == id);
p.Child = new Owned();
p.Child.Prop1 = "Test2";
p.Child.Prop2 = "Test2";
db.SaveChanges();
}
}
public class Principal
{
public Guid Id { get; set; }
public Owned Child { get; set; }
}
public class Owned
{
public string Prop1 { get; set; }
public string Prop2 { get; set; }
}
public class Ctx : DbContext
{
public DbSet<Principal> Principals { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=TestEF;Trusted_Connection=True;Persist Security Info=true");
}
protected override void OnModelCreating(ModelBuilder mb)
{
var emb = mb.Entity<Principal>();
emb
.OwnsOne(o => o.Child, cfg =>
{
cfg.Property(o => o.Prop1).HasMaxLength(30);
//cfg.WithOwner();
});
}
}
}
}

This is a bug, filed at https://github.com/aspnet/EntityFrameworkCore/issues/17422
As a workaround you could make the child appear as modified:
db.ChangeTracker.DetectChanges();
var childEntry = db.Entry(p.Child);
childEntry.State = EntityState.Modified;
db.SaveChanges();

Try this instead:
_context.Update(entity);
This will update all the owned properties so SaveChanges() updates them, too.

Related

Duplicating entities from EF Core into MongoDB on Creation or Edit

I'm working on an asp.net core project that has both sql server and mongodb. the task is to add or update any entity that is being added or updated to sql server(using ef core), to mongo.
I figured I can override OnSaveChangesAsync but I have no idea how to access the entities that are being changed there in order to call the mongo service's update and insert methods on them.
This can be done by using interceptors:
The Model:
public class Student
{
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
DbContext:
public class StudentsContext : DbContext
{
public DbSet<Student> Students { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.AddInterceptors(new UpdateMongoInterceptor())
.UseSqlite(#"DataSource=C:\Databases\Students.db");
}
}
SaveChangesInterceptor:
public class UpdateMongoInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateMongoDb(eventData.Context);
return result;
}
private static void UpdateMongoDb(DbContext context)
{
context.ChangeTracker.DetectChanges();
foreach (var entry in context.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
case EntityState.Added:
if (entry.Entity is Student student)
{
//Update MongoDB...
Console.WriteLine($"Push student entity to mongoDB Id: {student.Id}, Name:{student.Name}, Age:{student.Age}");
}
break;
}
}
}
Usege:
using (var context = new StudentsContext())
{
var student = new Student {Id = Guid.NewGuid(), Name = "Jon Snow", Age = 25};
context.Add(student);
context.SaveChanges();
}
Interceptors documentations can be found here: Example: SaveChanges interception for auditing
** If you are using DB generated keys you should override SavedChanges and figure out of the entity was updated or added.

EF Core 3.1: Navigation property doesn't lazy load entities when calling the backing field first

I am using EF Core 3.1.7. The DbContext has the UseLazyLoadingProxies set. Fluent API mappings are being used to map entities to the database. I have an entity with a navigation property that uses a backing field. Loads and saves to the database seem to work fine except for an issue when accessing the backing field before I access the navigation property.
It seems that referenced entities don't lazy load when accessing the backing field. Is this a deficiency of the Castle.Proxy class or an incorrect configuration?
Compare the Student class implementation of IsRegisteredForACourse to the IsRegisteredForACourse2 for the behavior in question.
Database tables and relationships.
Student Entity
using System.Collections.Generic;
namespace EFCoreMappingTests
{
public class Student
{
public int Id { get; }
public string Name { get; }
private readonly List<Course> _courses;
public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();
protected Student()
{
_courses = new List<Course>();
}
public Student(string name) : this()
{
Name = name;
}
public bool IsRegisteredForACourse()
{
return _courses.Count > 0;
}
public bool IsRegisteredForACourse2()
{
//Note the use of the property compare to the previous method using the backing field.
return Courses.Count > 0;
}
public void AddCourse(Course course)
{
_courses.Add(course);
}
}
}
Course Entity
namespace EFCoreMappingTests
{
public class Course
{
public int Id { get; }
public string Name { get; }
public virtual Student Student { get; }
protected Course()
{
}
public Course(string name) : this()
{
Name = name;
}
}
}
DbContext
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFCoreMappingTests
{
public sealed class Context : DbContext
{
private readonly string _connectionString;
private readonly bool _useConsoleLogger;
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public Context(string connectionString, bool useConsoleLogger)
{
_connectionString = connectionString;
_useConsoleLogger = useConsoleLogger;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter((category, level) =>
category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information)
.AddConsole();
});
optionsBuilder
.UseSqlServer(_connectionString)
.UseLazyLoadingProxies();
if (_useConsoleLogger)
{
optionsBuilder
.UseLoggerFactory(loggerFactory)
.EnableSensitiveDataLogging();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>(x =>
{
x.ToTable("Student").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasMany(p => p.Courses)
.WithOne(p => p.Student)
.OnDelete(DeleteBehavior.Cascade)
.Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Course>(x =>
{
x.ToTable("Course").HasKey(k => k.Id);
x.Property(p => p.Id).HasColumnName("Id");
x.Property(p => p.Name).HasColumnName("Name");
x.HasOne(p => p.Student).WithMany(p => p.Courses);
});
}
}
}
Test program which demos the issue.
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;
namespace EFCoreMappingTests
{
class Program
{
static void Main(string[] args)
{
string connectionString = GetConnectionString();
using var context = new Context(connectionString, true);
var student2 = context.Students.FirstOrDefault(q => q.Id == 5);
Console.WriteLine(student2.IsRegisteredForACourse());
Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities
Console.WriteLine(student2.IsRegisteredForACourse());
}
private static string GetConnectionString()
{
IConfigurationRoot configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
return configuration["ConnectionString"];
}
}
}
Console Output
False
True
True
When you declare a mapped property in an EF entity as virtual, EF generates a proxy which is capable of intercepting requests and assessing whether the data needs to be loaded. If you attempt to use a backing field before that virtual property is accessed, EF has no "signal" to lazy load the property.
As a general rule with entities you should always use the properties and avoid using/accessing backing fields. Auto-initialization can help:
public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();

Add include on DbContext level

I want to implement something similar to lazy loading, but don't understand how to implement that. I want to force entity framework core include navigation property for all queries for type which implements my interface
public interface IMustHaveOrganisation
{
Guid OrganisationId { get; set; }
Organisation Organisation { get; set; }
}
public class MyEntity : IMustHaveOrganisation {
public Guid OrganisationId { get; set; }
public virtual Organisation Organisation { get; set; }
}
Without lazy loading I need to add .Include(x=>x.Organisation) to each query literally , and I can't use implementation of lazy loading provided by Microsoft. I need kind of custom implementation of that with loading just one property.
Or even force DbContext somehow to Include that property, it also fine for me.
How can I achieve that?
You can make this work by rewriting the expression tree, before it gets translated by EF Core.
To make this work in a way, where you don't have to specify anything additional in the query, you can hook into the very beginning of the query pipeline and inject the Include() call as needed.
This can be done, by specifying a custom IQueryTranslationPreprocessorFactory implementation.
The following fully working console project demonstrates this approach:
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Organisation
{
public int OrganisationId { get; set; }
public string Name { get; set; }
}
public interface IMustHaveOrganisation
{
int OrganisationId { get; set; }
Organisation Organisation { get; set; }
}
public class MyEntity : IMustHaveOrganisation
{
public int MyEntityId { get; set; }
public string Name { get; set; }
public int OrganisationId { get; set; }
public virtual Organisation Organisation { get; set; }
}
public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
{
private readonly QueryTranslationPreprocessorDependencies _dependencies;
private readonly RelationalQueryTranslationPreprocessorDependencies _relationalDependencies;
public CustomQueryTranslationPreprocessorFactory(
QueryTranslationPreprocessorDependencies dependencies,
RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
{
_dependencies = dependencies;
_relationalDependencies = relationalDependencies;
}
public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
=> new CustomQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext);
}
public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
{
public CustomQueryTranslationPreprocessor(
QueryTranslationPreprocessorDependencies dependencies,
RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
QueryCompilationContext queryCompilationContext)
: base(dependencies, relationalDependencies, queryCompilationContext)
{
}
public override Expression Process(Expression query)
{
query = new DependenciesIncludingExpressionVisitor().Visit(query);
return base.Process(query);
}
}
public class DependenciesIncludingExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitConstant(ConstantExpression node)
{
// Call Include("Organisation"), if SomeEntity in a
// DbSet<SomeEntity> implements IMustHaveOrganisation.
if (node.Type.IsGenericType &&
node.Type.GetGenericTypeDefinition() == typeof(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<>) &&
node.Type.GenericTypeArguments.Length == 1 &&
typeof(IMustHaveOrganisation).IsAssignableFrom(node.Type.GenericTypeArguments[0]))
{
return Expression.Call(
typeof(EntityFrameworkQueryableExtensions),
nameof(EntityFrameworkQueryableExtensions.Include),
new[] {node.Type.GenericTypeArguments[0]},
base.VisitConstant(node),
Expression.Constant(nameof(IMustHaveOrganisation.Organisation)));
}
return base.VisitConstant(node);
}
}
public class Context : DbContext
{
public DbSet<MyEntity> MyEntities { get; set; }
public DbSet<Organisation> Organisations { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Register the custom IQueryTranslationPreprocessorFactory implementation.
// Since this is a console program, we need to create our own
// ServiceCollection for this.
// In an ASP.NET Core application, the AddSingleton call can just be added to
// the general service configuration method.
var serviceProvider = new ServiceCollection()
.AddEntityFrameworkSqlServer()
.AddSingleton<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>()
.AddScoped(
s => LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.BuildServiceProvider();
optionsBuilder
.UseInternalServiceProvider(serviceProvider) // <-- use our ServiceProvider
.UseSqlServer(#"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=62849896")
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyEntity>(
entity =>
{
entity.HasData(
new MyEntity {MyEntityId = 1, Name = "First Entity", OrganisationId = 1 },
new MyEntity {MyEntityId = 2, Name = "Second Entity", OrganisationId = 1 },
new MyEntity {MyEntityId = 3, Name = "Third Entity", OrganisationId = 2 });
});
modelBuilder.Entity<Organisation>(
entity =>
{
entity.HasData(
new Organisation {OrganisationId = 1, Name = "First Organisation"},
new Organisation {OrganisationId = 2, Name = "Second Organisation"});
});
}
}
internal static class Program
{
private static void Main()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
var myEntitiesWithOrganisations = context.MyEntities
.OrderBy(i => i.MyEntityId)
.ToList();
Debug.Assert(myEntitiesWithOrganisations.Count == 3);
Debug.Assert(myEntitiesWithOrganisations[0].Name == "First Entity");
Debug.Assert(myEntitiesWithOrganisations[0].Organisation.Name == "First Organisation");
}
}
}
Even though no explicit Include() is being made in the query in Main(), the following SQL is being generated, that does join and retrieve the Organisation entities:
SELECT [m].[MyEntityId], [m].[Name], [m].[OrganisationId], [o].[OrganisationId], [o].[Name]
FROM [MyEntities] AS [m]
INNER JOIN [Organisations] AS [o] ON [m].[OrganisationId] = [o].[OrganisationId]
ORDER BY [m].[MyEntityId]

EF Core: Only part of the model is saved to the database

I try to use EF core, but only a part of my model is saved to the database.
This is my model:
public class EngineType
{
public string Name { get; set; }
}
public class Car
{
public long CarId { get; set; }
public string Name { get; set; }
public EngineType Engine { get; set; }
}
The CarId and the Name is saved, but not the EngineType.
This is the test I use, but actual.Engine is always null:
[TestMethod]
public void WhenIAddAndSaveANewCarThenItIsAddedToDB()
{
using var target = new EFCoreExampleContext();
using var concurrentContext = new EFCoreExampleContext();
var expected = new Car() {CarId = 0815, Name = "Isetta", Engine = new EngineType() { Name = "2Takt" }};
target.Cars.Add(expected);
target.SaveChanges();
var actual = concurrentContext.Cars.Single();
Assert.AreEqual(1, concurrentContext.Cars.Count());
Assert.IsNotNull(actual.Engine);
Assert.AreEqual(expected, actual);
}
My Context looks like this:
public class EFCoreExampleContext : DbContext
{
public DbSet<Car> Cars { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase(databaseName: "Add_writes_to_database");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EngineType>(
d =>
{
d.HasKey(e => e.Name);
d.Property(e => e.Name).IsRequired();
});
modelBuilder.Entity<EngineType>(
d =>
{
d.HasKey(e => e.Name);
});
modelBuilder.Entity<Car>(
d =>
{
d.HasKey(e => e.CarId);
d.Property<DateTime>("LastChanged").IsRowVersion().ValueGeneratedOnAddOrUpdate();
d.Property<string>("EngineForeignKey");
d.HasOne(e => e.Engine)
.WithMany()
.HasForeignKey("EngineForeignKey")
.IsRequired();
});
}
}
Any idea what am I doing wrong (or which existing topic answers this question - I even didn't have the right search words to find it).
Thanks!
I think there is no issue with saving. Entity Framework does not do eager loading by default. So you have to explicitly include any navigational properties that should be in result. Try this when you are fetching actual,
using Microsoft.EntityFrameworkCore;
var actual = concurrentContext.Cars.Include(c => c.Engine).Single();

Seeding not working in Entity Framework Code First Approach

I am developing a .Net project. I am using entity framework code first approach to interact with database. I am seeding some mock data to my database during development. But seeding is not working. I followed this link - http://www.entityframeworktutorial.net/code-first/seed-database-in-code-first.aspx.
This is my ContextInitializer class
public class ContextInitializer : System.Data.Entity.CreateDatabaseIfNotExists<StoreContext>
{
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano" ,TotalSale = 1 });
brands.Add(new Brand { Name = "Nike" , TotalSale = 3 });
foreach(Brand brand in brands)
{
context.Brands.Add(brand);
}
base.Seed(context);
context.SaveChanges();
}
}
This is my context class
public class StoreContext : DbContext,IDisposable
{
public StoreContext():base("DefaultConnection")
{
Database.SetInitializer(new ContextInitializer());
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Brand> Brands { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
This is my brand class
public class Brand
{
public int Id { get; set; }
[Required]
[MaxLength(40)]
public string Name { get; set; }
public int TotalSale { get; set; }
}
I searched solutions online and I followed instructions. I run context.SaveChanges as well. But it is not seeding data to database. Why it is not working?
You are taking the wrong initializer, CreateDatabaseIfNotExists is called only if the database not exists!
You can use for example DropCreateDatabaseIfModelChanges:
Solution 1)
public class ContextInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<StoreContext>
{
You have to take care with this approach, it !!!removes!!! all existing data.
Solution 2)
Create a custom DbMigrationsConfiguration:
public class Configuration : DbMigrationsConfiguration<StoreContext>
{
public Configuration()
{
// Take here! read about this property!
this.AutomaticMigrationDataLossAllowed = true;
this.AutomaticMigrationsEnabled = false;
}
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano", TotalSale = 1 });
brands.Add(new Brand { Name = "Nike", TotalSale = 3 });
foreach (Brand brand in brands)
{
context.Brands.AddOrUpdate(m => m.Name, brand);
}
base.Seed(context);
context.SaveChanges();
}
}
In this way you can called( !!Before you create the DbContext or in the DbContext constructor!!):
// You can put me also in DbContext constuctor
Database.SetInitializer(new MigrateDatabaseToLatestVersion<StoreContext , Yournamespace.Migrations.Configuration>("DefaultConnection"));
Notes:
DbMigrationsConfiguration need to know about the connection string you can provide this info in the constructor or from outside.
In Your DbMigrationsConfiguration you can configure also:
MigrationsNamespace
MigrationsAssembly
MigrationsDirectory
TargetDatabase
If you leave everything default as in my example then you do not have to change anything!
Setting the Initializer for a Database has to happen BEFORE the context is ever created so...
public StoreContext():base("DefaultConnection")
{
Database.SetInitializer(new ContextInitializer());
}
is much to late. If you made it static, then it could work:
static StoreContext()
{
Database.SetInitializer(new ContextInitializer());
}
Your code is working if you delete your existing database and the EF will create and seeding the data
Or
You can use DbMigrationsConfiguration insted of CreateDatabaseIfNotExists and change your code as follow:
First you have to delete the existing database
ContextInitializer class
public class ContextInitializer : System.Data.Entity.Migrations.DbMigrationsConfiguration<StoreContext>
{
public ContextInitializer()
{
this.AutomaticMigrationDataLossAllowed = true;
this.AutomaticMigrationsEnabled = true;
}
protected override void Seed(StoreContext context)
{
IList<Brand> brands = new List<Brand>();
brands.Add(new Brand { Name = "Giordano", TotalSale = 1 });
brands.Add(new Brand { Name = "Nike", TotalSale = 3 });
foreach (Brand brand in brands)
{
context.Brands.AddOrUpdate(m => m.Name, brand);
}
base.Seed(context);
context.SaveChanges();
}
}
StoreContext
public class StoreContext : DbContext, IDisposable
{
public StoreContext() : base("DefaultConnection")
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<StoreContext, ContextInitializer>());
// Database.SetInitializer(new ContextInitializer());
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Brand> Brands { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
}
}
Then any change in your seed will automatically reflected to your database