In EF6 it was possible to define conventions based on property types during model building, like so...
public interface IEntity
{
Guid Id { get; }
}
public class MyEntity : IEntity
{
public Guid Id { get; set; }
}
public class MyDbContext : DbContext
{
public override void OnModelCreating(DbModelBuilder builder)
{
builder
.Properties<Guid>()
.Where(x => x.Name == nameof(IEntity.Id)
.Configure(a=>a.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity));
}
}
This approach could also be used to set default string length/null-ness, and so forth.
I have looked through the EF Core Model and associated types and can find no way of applying an equivalent convention in a way that is either enacted by the migration builder, or that does not cause migration builder to reject the model altogether. This is entirely frustrating and seems regressive.
Update
Adding the following to the OnModelCreating event...
foreach (var pb in builder.Model
.GetEntityTypes()
.Where(x=>typeof(IEntity).IsAssignableFrom(x.ClrType))
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(Guid) && p.Name == nameof(IEntity.Id))
.Select(p => builder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name)))
{
pb.UseSqlServerIdentityColumn();
}
...produces the following message on Add-Migration
Identity value generation cannot be used for the property 'Id' on entity type 'Tenant' because the property type is 'Guid'. Identity value generation can only be used with signed integer properties.
This does the job, but it's pretty inelegant.
foreach (PropertyBuilder pb in builder.Model
.GetEntityTypes()
.Where(x=>typeof(IEntity).IsAssignableFrom(x.ClrType))
.SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(Guid) && p.Name == nameof(IEntity.Id))
.Select(p => builder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name)))
{
pb.ValueGeneratedOnAdd().HasDefaultValueSql("newsequentialid()");
}
Related
I'm using dotnet 6, and I want to make a global query filter in ef core to check my authentication ID dynamically, but the OnModelCreating runs just once in the lifecycle of ASP.net. Do you have any ideas how to do this?
This is my OnModelCreating method. DeletedAt type is DateTime? and my authentication ID is CompanyId.
I get _companyId from the constructor and the type of it is long?. When a request is triggered, this ID fills, but my idea's problem is this method called at the startup of the project! At that time, the _companyId is null and never called again!
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyQueryFilters<IDeletedAt>(x => x.DeletedAt == null);
modelBuilder.ApplyQueryFilters<IDeletedAtAndCompany>(x => x.DeletedAt == null && x.CompanyId == _companyId);
}
This is my ApplyQueryFilters method if you need:
public static void ApplyQueryFilters<T>(this ModelBuilder modelBuilder, Expression<Func<T, bool>> expression)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (entityType.ClrType.GetInterface(typeof(T).Name) == null) continue;
var newParam = Expression.Parameter(entityType.ClrType);
var newBody = ReplacingExpressionVisitor
.Replace(expression.Parameters.Single(), newParam, expression.Body);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(newBody, newParam));
}
}
I think I need a new idea to resolve this issue.
Add the companyId to your Db Context then initialize it when you create the DbContext or when authenticate (you should have a DbContext peer session) :
public class ApplicationDbContext
{
...
public long? _companyId ;
}
in your OnModelCreating add this default filter (adapt it to your case) :
modelBuilder.Entity<YourEntity>().HasQueryFilter(b => EF.Property<string>(b, "companyId") == _companyId );
change the _companyId when needed.
I have an aggregate root with a non primitve type primary key :
public class Category : AggregateRoot<CategoryId>
{
// some properties
}
public class CategoryId : ValueObject<CategoryId>
{
public Guid Identity { get; private set; }
}
And my entity configuration :
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasConversion(
v => v.Identity,
v => new CategoryId(v));
builder.ToTable("Categories");
}
}
I have no problem in insert new data but when I want to receive data from database with following code, I received an error from Ef core 3.1.4 :
var categories = _context.Categories.FirstOrDefault(category=> category.Id.Identity == "someId");
Error : failed The LINQ expression 'DbSet<Category>
.Where(t => t.Id.Identity == __categoryId_0)' could not be translated.
How can I receive data from database without need to change my primary key to a primitive type?
I finally solve the problem using comparing aggregate Id with a new valueobject in the FirstOrDefault:
var category = _context.Categories.FirstOrDefault(c=> c.Id == new CategoryId("someId"));
In .NET I would normally be able to specify naming conventions on all columns and tables in the following way
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Properties()
.Where(p => p.Name == p.DeclaringType.Name + "_ID")
.Configure(p => p.IsKey());
base.OnModelCreating(modelBuilder);
}
But in .NET Core I can't find a way of doing this. When I override OnModelCreating() in the XYZContext that inherits from DbContext in .NET core, I only have a ModelBuilder
Is there a way of doing the above in .NET core or do I have to specify each column manually?
ModelBuilder allows similar functionality:
var keyProperties = modelBuilder
.Model
.GetEntityTypes()
.SelectMany(e => e.GetProperties())
.Where(p => p.Name == p.DeclaringEntityType.ClrType.Name + "_ID")
.ToList();
foreach (var p in keyProperties)
{
modelBuilder
.Entity(p.DeclaringEntityType.Name)
.HasKey(p.Name);
}
It's built into ModelBuilder.
Call modelBuilder.Model.GetEntityTypes() to get the types
To change the table name then use the following line of code
entityType.Relational().TableName = ConvertToUpperCaseUnderscore(entityType.ClrType.Name);
To map the properties to a columns using the same convention, call entityType.GetProperties() to get the list of properties, and map the column name.
So, overall usage is
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (entityType.ClrType == null)
{
continue;
}
// Set the table name mapping for the class
entityType.Relational().TableName = ConvertToUpperCaseUnderscore(entityType.ClrType.Name);
var props = entityType.GetProperties().ToList();
foreach (var p in props)
{
// Set the column name mapping for the class
p.Relational().ColumnName = ConvertToUpperCaseUnderscore(p.Name);
}
}
base.OnModelCreating(modelBuilder);
}
I have an entity with ID field
public class AgentOrder : BaseEntity
{
public int Id { get; set; }
}
public class AgentOrderMap : EntityTypeConfiguration<AgentOrder>
{
public AgentOrderMap()
{
ToTable("AgentOrder");
HasKey(m => m.Id);
Property(m => m.Status);
}
}
I create a new entity and define Id property as well. But when I save changes the new entity is created not with Id I've defined but with automatically generated one.
Is it normal behavior of key property?
Since you are using Fluent Configurations, you just need to turn off the default identity insert configuration:
So this:
public AgentOrderMap()
{
ToTable("AgentOrder");
HasKey(m => m.Id);
Property(m => m.Status);
}
Becomes:
public AgentOrderMap()
{
ToTable("AgentOrder");
HasKey(m => m.Id);
Property(m => m.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
Property(m => m.Status);
}
Note that your database needs to support this functionality and have identity inserts turned off as well.
By convention, the id property uses Identity, so, it cannot be manually inserted. Configure it like this:
Property(l => l.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
I need to transform some existing DB under a new application using EF6 with the CodeFirst approach. I am fighting with mapping conventions in inheritance. There is a minimal (not)working example:
Assume I have two tables: a Parent and a Child.
CREATE TABLE A_PARENT (
A_PAR_ParentId UNIQUEIDENTIFIER PRIMARY KEY,
A_PAR_data VARCHAR(255)
)
CREATE TABLE B_CHILD (
B_CHL_ChildId UNIQUEIDENTIFIER PRIMARY KEY FOREIGN KEY REFERENCES A_PARENT(A_PAR_ParentId),
B_CHL_childData VARCHAR(255)
)
I successfuly solved the table prefices you can see, such the "A_PAR" using custom attributes. I believe the EF exactly knows what property belongs to which column and which one is the primary key. Everything works fine except for inheritance. Because when I attempt to get all of Children, I end up with a SQL error, because the EF produces a query like this:
SELECT
'0X0X' AS [C1],
[Extent1].[A_PAR_ParentId] AS [A_PAR_ParentId],
[Extent1].[A_PAR_Data] AS [A_PAR_Data],
[Extent2].[B_CHL_ChildId] AS [B_CHL_ChildId],
[Extent2].[B_CHL_ChildData] AS [B_CHL_ChildData]
FROM [dbo].[A_PARENT] AS [Extent1]
INNER JOIN [dbo].[B_CHILD] AS [Extent2] ON [Extent1].[A_PAR_ParentId] = [Extent2].[A_PAR_ParentId]
The only incorrect thing in the query is the join predicate - there is no such a column A_PAR_ParentId in the table B_CHILD.
How can I force the EntityFramework to use entity's primary key to use as a foreign key when constructing the inheritance chain? I am looking for some general solution based on conventions because all of the tables in the DB are using this pattern (if the type/table is inherited => the primary key is a foreign key to parent's primary key and there are no composite keys at all). Maybe I am looking for some way to tell the EF the PK is also a FK but without a navigation property.
--Edit: more code
The model is pretty simple:
[ModulePrefix("A"), TablePrefix("PAR")]
public class Parent
{
public Guid ParentId { get; set; }
public string Data { get; set; }
}
[ModulePrefix("B"), TablePrefix("CHL")]
public class Child : Parent
{
public Guid ChildId { get; set; }
public string ChildData { get; set; }
}
And the configuration:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Types()
.Where(type => !type.GetCustomAttributes(false).OfType<TableAttribute>().Any() && type.GetCustomAttributes(false).OfType<ModulePrefixAttribute>().Any())
.Configure(config => config.ToTable(
ComposeDbName(GetModulePrefix(config.ClrType),
CamelCaseToUnderscore(GetClassName(config.ClrType)).ToUpper())
));
modelBuilder.Properties()
.Where(property => property.DeclaringType.GetCustomAttributes(false).OfType<TablePrefixAttribute>().Any() && property.DeclaringType == property.ReflectedType)
.Configure(config => config.HasColumnName(ComposeDbName(
GetModulePrefix(config.ClrPropertyInfo.DeclaringType),
GetTablePrefix(config.ClrPropertyInfo.DeclaringType),
config.ClrPropertyInfo.Name
)));
modelBuilder.Properties()
.Where(property => property.Name == property.DeclaringType.Name + "Id" && property.ReflectedType == property.DeclaringType)
.Configure(config => config.IsKey());
modelBuilder.Properties()
.Where(property => property.PropertyType.IsClass || Nullable.GetUnderlyingType(property.PropertyType) != null)
.Configure(config => config.IsOptional());
}
There are some more private methods playing with columns/tables names. I think thats not important at this point since column/table names appear to be resolved well.
if the type/table is inherited => the primary key is a foreign key to parent's primary key
This is exactly how EF TPT inheritance mapping works, so no conflict here.
The problem is the ChildId property in Child class. Since Child inherits Parent, it also inherits ParentId property, ending up with incorrect TPT mapping. You have to remove it and use the base class defined property as PK (and FK), just give it a different name in the derived table.
I would suggest you using Id as name for the PK property to allow easily associating different name by convention.
So here is how the corrected model would look like:
[ModulePrefix("A"), TablePrefix("PAR")]
public class Parent
{
public Guid Id { get; set; }
public string Data { get; set; }
}
[ModulePrefix("B"), TablePrefix("CHL")]
public class Child : Parent
{
public string ChildData { get; set; }
}
and the configuration based on that convention:
modelBuilder.Types()
.Where(type => !type.GetCustomAttributes(false).OfType<TableAttribute>().Any() && type.GetCustomAttributes(false).OfType<ModulePrefixAttribute>().Any())
.Configure(config => config.ToTable(GetTableName(config.ClrType)));
modelBuilder.Properties()
.Where(property => property.Name != "Id" && property.DeclaringType.GetCustomAttributes(false).OfType<TablePrefixAttribute>().Any() && property.DeclaringType == property.ReflectedType)
.Configure(config => config.HasColumnName(GetColumnName(config.ClrPropertyInfo.DeclaringType, config.ClrPropertyInfo.Name)));
modelBuilder.Properties()
.Where(property => property.Name == "Id" && property.DeclaringType.GetCustomAttributes(false).OfType<TablePrefixAttribute>().Any())
.Configure(config => config.IsKey().HasColumnName(GetColumnName(config.ClrPropertyInfo.ReflectedType, "Id")));
which uses two new private helpers in addition to yours:
static string GetTableName(Type entityType)
{
return ComposeDbName(
GetModulePrefix(entityType),
CamelCaseToUnderscore(GetClassName(entityType)).ToUpper()
);
}
static string GetColumnName(Type entityType, string propertyName)
{
return ComposeDbName(
GetModulePrefix(entityType),
GetTablePrefix(entityType),
propertyName == "Id" ? GetClassName(entityType) + "Id" : propertyName
);
}
What it does is to build conventionally the equivalent of this:
modelBuilder.Entity<Parent>().ToTable("A_PARENT");
modelBuilder.Entity<Parent>().Property(e => e.Id).HasColumnName("A_PAR_ParentId");
modelBuilder.Entity<Parent>().HasKey(e => e.Id);
modelBuilder.Entity<Parent>().Property(e => e.Data).HasColumnName("A_PAR_Data");
modelBuilder.Entity<Child>().ToTable("B_CHILD");
modelBuilder.Entity<Child>().Property(e => e.Id).HasColumnName("B_CHL_ChildId");
modelBuilder.Entity<Child>().HasKey(e => e.Id);
modelBuilder.Entity<Child>().Property(e => e.ChildData).HasColumnName("B_CHL_ChildData");