Summary / Detailed entities without discriminator - c#

I want to have a single table where I can retrieve data using either a summary entity or a detailed entity. All data will be added using the detailed entity but often I just need a summary of each record for building a list and don't need some of the larger fields to be loaded for this for speed. I've tried the code below but it adds in a discriminator, which I don't want. How can I do this properly?
Ideally it would prevent records being added with the summary but I'm not so concerned about that.
public class Summary
{
public Guid Id { get; set; }
[StringLength(254)]
public string Name { get; set; } = string.Empty;
}
public class Detailed : Summary
{
public string BigField { get; set; } = string.Empty;
}
public class MyDbContext : DbContext
{
public DbSet<Summary> Summaries { get; set; } = null!;
public DbSet<Detailed> Details { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Summary>(
s =>
{
s.ToTable("Details");
});
modelBuilder.Entity<Detailed>(
s =>
{
s.ToTable("Details");
});
}
}

I think what you're looking for is called Table per hirarchy:
https://learn.microsoft.com/en-us/ef/core/modeling/inheritance#table-per-type-configuration
another solution would be to add the Detailed to the db and only select the needed fields in your queries
dbContext
.Details
.Select(d => new Summary { Id = d.Id, Name = d.Name })
.ToListAsync();
this gets translated to the sql query:
SELECT Id, Name
From Detailed
and therefore you don't query all the data.

Related

Use dynamic linq filters in the Include and ThenInclude part of a entity framework core query

What i want to achieve:
Currently I'm working on a filter system which will based on user input apply different filters on entites. That is an easy task if you just focus on the root entity or on Many-to-one relationships. But as soon as you want to filter on collections it gets a little bit harder to express and also harder to query.
The Problem:
I want to filter on the root entity (Organization) and also want to filter on collections like Organization.Contracts or Contract.Licenses. I have the option to add a selection to the Include and ThenInclude clause (see my example). But i can only add fixed LINQ-Querys but not build dynamic functions to select the right rows.
The Exception:
System.ArgumentException: "Expression of type 'System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]]' of method 'System.Linq.IQueryable`1[ExpressionTreeTest.MinimalTest+Contract] Where[Contract](System.Linq.IQueryable`1[ExpressionTreeTest.MinimalTest+Contract], System.Linq.Expressions.Expression`1[System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]])' Arg_ParamName_Name"
Based on the exception i saw, that Entity Framework Core wants a System.Linq.Expressions.Expression1[System.Func2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]] but i provide a System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]. As soon as i change this to Expression<Func<Contract, bool>> ContractFilterExpression = c => c.EndDate > DateTime.Now.AddMonths(11); intellisense reports an error, that this can not be accepted.
Other awnsers:
I found many different questions about querying entity framework core and building dynamic queries. Based on this I was able to build my dynamic queries for the root entity (Organization). But for the nested ThenInclude list queries i could not find any dynamic example.
My minimal test case for you:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
namespace ExpressionTreeTest
{
public class MinimalTest
{
public void RunTest()
{
AppDbContext dbContext = new AppDbContext();
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
// Setup some test data to check the query results
SetupTestData(dbContext);
// Pre build query with entity framework, which is working as expected
// Expectation:
// Get all Organizations with the Name == NameOfCompany and
// Include all Organization.Contracts which are Created > ateTime.Now.AddMonths(12)
// ThenInclude all Contract.Licenses which are l.Articel.Name == "TestArticelName"
IQueryable<Organization> preBuildQuery = dbContext.Organizations.Where(o => o.Name == "NameOfCompany").
Include(c => c.Contracts.Where(c => c.Created > DateTime.Now.AddMonths(12))).
ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));
// This prints 1, which is the desired result
Console.WriteLine("Query result count: " + preBuildQuery.ToList().Count());
// This is the dynamic filter funtion for the Include-Part of the query
// This function gets accepted by Visual Studio but throws an error by Entity Framework
Func<Contract, bool> ContractFilterFunction = c => c.EndDate > DateTime.Now.AddMonths(11);
// Build the above query dynamically based on user input
IQueryable<Organization> dynamicQuery = dbContext.Organizations.Where(BuildWhereQuery()).
Include(c => c.Contracts.Where(ContractFilterFunction)).
ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));
// This is the line with the error you will find in the question
// If i remove the ContractFilterFunction and replace it with an inline lambda
// the query gets executed, but i am not able to dynamically set the query parameters.
Console.WriteLine("Query result count: " + dynamicQuery.ToList().Count());
}
/// <summary>
/// This method creates based on input a query with different types. In the future there
/// should be some select based on the binaryExpression to use (Equal, Greater, etc.)
/// At the moment this is static for testing purposes
/// </summary>
/// <returns>A Func<T,bool> to parse to a Linq-Entity-Framewor query</returns>
private Expression<Func<Organization, bool>> BuildWhereQuery()
{
ParameterExpression rootEntity = Expression.Parameter(typeof(Organization));
MemberExpression fieldExpression = Expression.PropertyOrField(rootEntity, "Name");
ConstantExpression valueToCompare = Expression.Constant("NameOfCompany");
var binaryExpression = Expression.Equal(fieldExpression, valueToCompare);
return Expression.Lambda<Func<Organization, bool>>(binaryExpression, rootEntity);
}
public class AppDbContext : DbContext
{
public DbSet<Organization> Organizations { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<Contract> Contracts { get; set; }
public DbSet<License> Licenses { get; set; }
public DbSet<Articel> Articels { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(#"Data Source=mydb.db");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
public class BasicEntity
{
[Key]
public Guid Id { get; set; }
}
public class Organization : BasicEntity
{
public string Name { get; set; }
public List<Contract> Contracts { get; set; }
public List<Contact> Contacts { get; set; }
}
public class Articel : BasicEntity
{
public string Name { get; set; }
}
public class Contact : BasicEntity
{
public string Name { get; set; }
public Organization Organization { get; set; }
}
public class Contract : BasicEntity
{
public DateTime Created { get; set; }
public DateTime EndDate { get; set; }
public List<License> Licenses { get; set; }
public Organization Organization { get; set; }
}
public class License : BasicEntity
{
public string LicenseNumber { get; set; }
public Articel Articel { get; set; }
public Contract Contract { get; set; }
}
private static void SetupTestData(AppDbContext dbContext)
{
Organization org = new Organization
{
Name = "NameOfCompany",
};
dbContext.Add(org);
dbContext.Add(new Contact
{
Name = "Contact 1",
Organization = org
});
dbContext.Add(new Contact
{
Name = "Contact 2",
Organization = org
});
Articel articel = new Articel
{
Id = Guid.NewGuid(),
Name = "TestArticelName"
};
dbContext.Add(articel);
Contract contract = new Contract
{
Id = Guid.NewGuid(),
Created = DateTime.Now,
EndDate = DateTime.Now.AddMonths(12),
Organization = org
};
dbContext.Add(contract);
License license = new License
{
Id = Guid.NewGuid(),
LicenseNumber = "12345-12345",
Articel = articel,
Contract = contract
};
dbContext.Add(license);
dbContext.SaveChanges();
}
}
}
Note:
If you have any additional tips for me or even a workaround or other solution, I would be very happy. It's not a requirement to do it like this, but it's the only way I found.
Simplest way to solve your issue is to install LINQKit - LinqKit.Microsoft.EntityFrameworkCore
Add WithExpressionExpanding in OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(#"Data Source=mydb.db");
optionsBuilder.WithExpressionExpanding();
}
Make dynamic filter function as a Expression, EF Core will not translate Func<> to the SQL and use LINQKit extension Invoke:
// This is the dynamic filter function for the Include-Part of the query
Expression<Func<Contract, bool>> ContractFilterFunction = c => c.EndDate > DateTime.Now.AddMonths(11);
IQueryable<Organization> dynamicQuery = dbContext.Organizations.Where(BuildWhereQuery())
.Include(c => c.Contracts.Where(c => ContractFilterFunction.Invoke(c)))
.ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));
LINQKit extension will expand LambdaExpression ContractFilterFunction and inject into final Expression Tree before processing by EF Core's LINQ Translator.

Entity Framework Core: Check if an entity part of a One-to-Many relationship exists on Add

I am very new to Entity Framework Core (Entity Framework in General), and I have watched several tutorial videos and done a Pluralsight course.
I am using ASP.NET Web API, and I want to add a new entity, that has a One-to-Many relationship. The models are as follows:
"Parent" Class:
public partial class VerificationVoltage
{
public int Id { get; set; }
public DateTime Date { get; set; }
public string Type { get; set; }
public int VerificationVoltageSerialId { get; set; }
public VerificationVoltageSerial Serial { get; set; }
}
"Child" Class:
public class VerificationVoltageSerial
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(20)]
public string Serial { get; set; }
public List<VerificationVoltage> VerificationVoltage { get; set; }
}
I would like the VerificationVoltageSerial.Serial to be unique, and that the database will first check whether or not the serial exists, before adding the newly added serial (The serials themselves are unique).
Is there a way to do the following: If a row with the same serial exist, as what needs to be added, entity framework then automatically selects the Serial and SerialId and populates the Entity that is being added, with the serial selected from the database.
This is what I am doing currently, I feel that this is a very manual check, an maybe there is something more automatic: (i.e I want it to wrok, even if I remove the IF Statement and the exisitngEntry query in the controller)
[HttpPost]
public async Task<VerificationVoltage> AddVerificationVoltage(VerificationVoltage verificationVoltage)
{
var exisitngEntry = await repository.GetBySerialNoTracking(verificationVoltage.Serial.Serial)
.ConfigureAwait(false);
if (exisitngEntry != null)
{
var existingSerial = exisitngEntry.Serial;
verificationVoltage.Serial.Id = existingSerial.Id;
verificationVoltage.Serial = new VerificationVoltageSerial()
{
Id = existingSerial.Id,
Serial = existingSerial.Serial,
};
}
var addedEntry = repository.Add(verificationVoltage);
await repository.SaveChanges().ConfigureAwait(false);
return addedEntry;
}
public async Task<VerificationVoltage> GetBySerialNoTracking(string serialNumber)
{
return await DbSet.Include(a => a.Serial)
.Where(a => a.Serial.Serial.Equals(serialNumber))
.OrderBy(a => a.VerificationVoltageSerialId)
.AsNoTracking()
.FirstOrDefaultAsync()
.ConfigureAwait(false);
}
My protected override void OnModelCreating(ModelBuilder modelBuilder) method, only repeats the attributes:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ValidationLabAmbientMeasurements>()
.HasOne(a => a.AmbientMeasurementsIdentifier)
.WithMany();
modelBuilder.Entity<VerificationVoltage>()
.Property(a => a.Id)
.ValueGeneratedOnAdd();
modelBuilder.Entity<VerificationVoltage>()
.HasOne(a => a.Serial)
.WithMany(nameof(VerificationVoltage));
modelBuilder.Entity<VerificationVoltage>()
.HasMany(a => a.VerificationVoltageMeasurements)
.WithOne(nameof(VerificationVoltage));
}
I have tried searching for answers, but my search queries do not get results on this specific issue.

EF6: Code First Complex Type

I'm having trouble getting entity framework to flatten my domain entity classes with Value Objects (complex type) fields to one table.
Everything works if I tell my model builder to ignore my value objects/complex type, but that results in all the attributes of the value object being missed in my tables. As soon as I remove the ignore statement i get "A value shared across entities is created in more than one location". If I look in the resulting CE SQL file I see an additional table named after my Domain class appended with a 1 and containing only the Value Object parameters.
Some Code:
My domain Classes:
public User {
private User(){}
public long Id {get; private set;} // dont ask, inherited legacy database
public string UserId { get; private set; }
public string Domain { get; private set; }
public AuditIformation AuditDetails {get ; private set;}
//..domain logic etc
}
public AuditInformation : IValueObject {
public long CreatedByUserId { get; private set; }
public DateTime CreatedDate { get; private set; }
}
My repository project (going code first) has got this:
public partial class myContext : DbContext {
protected override void OnModelCreating(DbModelBuilder mb) {
mb.Conventions.Remove<PluralizingTableNameConvention>();
mb.ComplexType<Domain.Model.AuditInformation>();
mb.ComplexType<Domain.Model.AuditInformation>().Property(a => a.CreatedDate).HasColumnName("Created_On");
mb.ComplexType<Domain.Model.AuditInformation>().Property(a => a.CreatedByUserId).HasColumnName("Created_By");
//This line lets everything work but doesn't include my
//AuditInformation attributes in my User Table.
mb.Ignore<Domain.Model.AuditInformation>(); // <== I think I need to remove this
//..
mb.Entity<User>().Map(a => {
a.Property(x => x.Id).HasColumnName("Id");
a.Property(x => x.UserId).HasColumnName("User_Id");
a.Property(x => x.Domain).HasColumnName("User_Dmain");
})
.HasKey(x => x.Id)
.ToTable("Tbl_User"); //<==Again, dont ask
}
}
What I want to get is a table looking like:
[TBL_USER]
ID AS BIGINT,
USER_ID as VARCHAR(MAX),
USER_DMAIN AS VARCHAR(MAX),
CREATED_ON as DATE,
CREATED_BY as BIGINT
But what im getting is only:
[TBL_USER]
ID AS BIGINT,
USER_ID as VARCHAR(MAX),
USER_DMAIN AS VARCHAR(MAX),
and if I remove the ignore line i get this bonus freak table
[USER1] <<==Note, named after the domain class, not the destination table..
ID AS BIGINT,
CREATED_ON as DATE,
CREATED_BY as BIGINT
and a whole bunch of error when I try to use my repository:
----> System.Data.Entity.Infrastructure.DbUpdateException : A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple store-generated columns.
----> System.Data.Entity.Core.UpdateException : A value shared across entities or associations is generated in more than one location. Check that mapping does not split an EntityKey to multiple store-generated columns.
----> System.ArgumentException : An item with the same key has already been added.
TearDown : System.NullReferenceException : Object reference not set to an instance of an object.
Ive done a lot of searching but I just cant find any concrete examples of persisting my value object attributes into the tables created for my domain objects. Can someone show me where I'm going wrong?
Try this:
public class AuditInformation
{
public long CreatedByUserId { get; set; }
public DateTime CreatedDate { get; set; }
}
public abstract class AuditInfo
{
public AuditInformation AuditDetails { get; set; }
public AuditInfo()
{
this.AuditDetails = new AuditInformation();
this.AuditDetails.CreatedByUserId = 0;
this.AuditDetails.CreatedDate = DateTime.Now;
}
}
public User : AuditInfo
{
private User(){}
public long Id {get; private set;} // dont ask, inherited legacy database
public string UserId { get; private set; }
public string Domain { get; private set; }
//..domain logic etc
}
public partial class myContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder mb)
{
mb.Conventions.Remove<PluralizingTableNameConvention>();
mb.ComplexType<Domain.Model.AuditInformation>();
mb.ComplexType<Domain.Model.AuditInformation>().Property(a => a.CreatedDate).HasColumnName("Created_On");
mb.ComplexType<Domain.Model.AuditInformation>().Property(a => a.CreatedByUserId).HasColumnName("Created_By");
mb.Entity<Cricketer>().Map(a =>
{
a.Property(x => x.Id).HasColumnName("Id");
a.Property(x => x.UserId).HasColumnName("User_Id");
a.Property(x => x.Domain).HasColumnName("User_Dmain");
a.Property(x => x.AuditDetails.CreatedByUserId).HasColumnName("CreatedByUserId");
a.Property(x => x.AuditDetails.CreatedDate).HasColumnName("CreatedDate");
})
.HasKey(x => x.ID)
.ToTable("Tbl_User"); //<==Again, dont ask
}
}

EF Code First working with many-to-many relationships

I am using Visual Studio 2010, C# 4.0 and Entity Framework 5.0. I have been using database first development for many years but am trying to move to code first and am running into problems. Reading and searching does not seem to address the problems
I have simplified my problem as follows - I have two classes - Assessors and Documents.
public class Assessor
{
public int AssessorID { get; set; }
public virtual List<Document> Documents { get; set; }
}
public class Document
{
public int DocumentID { get; set; }
public string DocumentLocation { get; set; }
public string DocumentName { get; set; }
public virtual List<Assessor> Assessors { get; set; }
}
with the context
public class DocumentAssignment : DbContext
{
public DbSet<Assessor> Assessors { get; set; }
public DbSet<Document> Documents { get; set; }
}
An assessor can have many documents and a document can have many assessors (a classic many-to-many relationship).
I am using convention to create the relationship but have also used the fluent API. I have seeded the document table.
My two questions:
ONE - I want to assign documents to assessors - what is the best way to save this to the database?
TWO I have the following method to retrieve documents assigned to an assessor:
public static IEnumerable<MaternalDocument> GetAssignedDocumentList(int UserID, string ConnectionString)
{
using (DocumentAssignment dbContext = new DocumentAssignment(ConnectionString))
{
return returnValue = dbContext.MaternalAssessments
.Where(m => m.AssessorID == UserID)
.Include(m => m.MaternalDocuments)
.Select(m => m.MaternalDocuments)
.ToList();
}
}
but I cannot get this to compile because of mapping issues. What am I doing wrong?
You have to tell the DbContext about how the many-to-many relationship is set up, by overriding OnModelCreating in DocumentAssignment. Replace AssessorDocuments in this code with your relation table name.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Assessor>()
.HasMany(a => a.Documents)
.WithMany(d => d.Assessors)
.Map(m =>
{
m.MapLeftKey("AssessorID");
m.MapRightKey("DocumentID");
m.ToTable("AssessorDocuments");
});
base.OnModelCreating(modelBuilder);
}
To assign a Document to an Assessor (assuming a Document exists with DocumentID of 1 and an Assessor exists with an AssessorID of 1):
using (var context = new DocumentAssignment())
{
var assessor = context.Assessors.Find(1);
var document = context.Documents.Find(1);
assessor.Documents.Add(document);
context.SaveChanges();
}
Your GetAssignedDocumentList method would look something like this:
public static IEnumerable<Document> GetAssignedDocumentList(int UserID)
{
using (var context = new DocumentAssignment())
{
return context.Documents.Where(d => d.Assessors.Any(a => a.AssessorID == UserID));
}
}

Difficulty Concerning EF Code First Fluent API, TPH, and Foreign Keys

I have two tables in my database. One is called Users, and the other is called Widgets. The Widgets table represents 3 entities in my code model. One of the entities, Widget, is a parent class for the other two entities, WidgetTypeA and WidgetTypeB. Both WidgetTypeA and WidgetTypeB have navigation properties to the User entity, which is persisted to the Users table in the database. I'm having trouble getting Code First to use the same foreign key for both the WidgetTypeA and WidgetTypeB entities (UserId). Does anyone know how to do this? It seems like it should be a common problem with Table Per Hierarchy mapping.
My entity classes are as follows:
public class Widget
{
public int Id { get; set; }
public string Name { get; set; }
}
class WidgetMap : EntityTypeConfiguration<Widget>
{
public WidgetMap()
{
ToTable("Widgets");
HasKey(w => w.Id);
Property(w => w.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(w => w.Name)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
}
}
public class WidgetTypeA : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
public string Color { get; set; }
public int DepthLevel { get; set; }
}
class WidgetTypeAMap : EntityTypeConfiguration<WidgetTypeA>
{
public WidgetTypeAMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(1));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeAs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
Property(w => w.Color)
.IsOptional()
.IsUnicode(true)
.HasMaxLength(75);
Property(w => w.DepthLevel)
.IsOptional();
}
}
public class WidgetTypeB : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
}
class WidgetTypeBMap : EntityTypeConfiguration<WidgetTypeB>
{
public WidgetTypeBMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(2));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeBs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
}
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int Age { get; set; }
public virtual ICollection<WidgetTypeA> WidgetTypeAs { get; set; }
public virtual ICollection<WidgetTypeB> WidgetTypeBs { get; set; }
}
class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
ToTable("Users");
HasKey(u => u.Id);
Property(u => u.Username)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
Property(u => u.Age)
.IsRequired();
}
}
At any rate, I keep getting the error
Invalid column name 'UserId1'
when I try to perform the following operations:
using (var entities = new MyEntities())
{
User u = new User
{
Username = "Frank",
Age = 14
};
entities.Users.Add(u);
entities.SaveChanges();
WidgetTypeA wa1 = new WidgetTypeA
{
Name = "0SDF81",
UserId = u.Id,
DepthLevel = 6
};
entities.WidgetTypeAs.Add(wa1);
entities.SaveChanges();
}
Not sure if this can be fixed or not. I can always specify a second UserId foreign key for the Widgets table, but that seems pointless. Perhaps there's a way to do this using Fluent API?
You cannot map properties defined in different derived entities to the same column. That is limitation in EF. If your WidgetTypeA has UserId property and your WidgetTypeB has UserId property they must be different columns in the database. It should work if you move both UserId and User properties from derived types to the parent Widget type.
I know its a long way late, but hopefully may help other readers.
Although Ladislav was correct that using a mapped Foreign Key is not supported in EF6, I did find a useful workaround.
It is possible to define a computed column specification whose expression simply refers to the original column. Userid in the description above. This can be used as the discriminator for the TPH mapping. With this approach, the column need not be persisted, but can be used for TPH, with the original column being available for use as a foreign key.

Categories