In this question I asked for a way to achieve Property/Field level access in .NET 5. One idea was to do the magic in the EF Core configuration, so that only those properties are loaded from database the current user has access to. In the second step the empty property will the excluded from the dto using the JsonIgnore attribute with default condition. I've tried my best to implement the logic in the OnModelCreating hook of the context using a custom property attribute on the entity properties:
public class MyEntity : BaseEntity
{
[IncludeForRoles(RoleNames.Staff)]
public string InternalDetails { get; private set; }
}
public class MyContext : IdentityDbContext<User, Role, Guid>, IMyContext
{
private readonly ITokenAccessor _tokenAccessor;
public MyContext(
DbContextOptions options,
ITokenAccessor tokenAccessor) : base(options)
{
_tokenAccessor = tokenAccessor;
}
[...]
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(typeof(UserConfiguration).Assembly);
foreach (Type type in MyContextUtility.GetEntityTypes())
{
MethodInfo ignoreFlagMethod = MyContextUtility.SetRoleBasedIgnoreFlagMethod.MakeGenericMethod(type);
ignoreFlagMethod.Invoke(this, new object[] { builder, _tokenAccessor.UserRoles });
}
}
}
public static class MyContextUtility
{
private static IList<Type> _entityTypeCache;
public static IList<Type> GetEntityTypes()
{
if (_entityTypeCache != null)
{
return _entityTypeCache.ToList();
}
Assembly assembly = typeof(BaseEntity).Assembly;
_entityTypeCache = (from t in assembly.DefinedTypes
where t.BaseType == typeof(BaseEntity)
select t.AsType()).ToList();
return _entityTypeCache;
}
public static readonly MethodInfo SetRoleBasedIgnoreFlagMethod
= typeof(MyContextUtility).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Single(t => t.IsGenericMethod && t.Name == nameof(SetRoleBasedIgnoreFlag));
public static void SetRoleBasedIgnoreFlag<T>(ModelBuilder builder, IList<string> userRoles) where T : BaseEntity
{
IEnumerable<PropertyInfo> props = typeof(T).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(IncludeForRolesAttribute)));
foreach (PropertyInfo prop in props)
{
IncludeForRolesAttribute attr = prop.GetCustomAttribute<IncludeForRolesAttribute>();
if (!userRoles.Intersect(attr.RoleNames).Any())
{
Debug.WriteLine($"Adding ignore flag for type '{typeof(T).Name}' and property {prop.Name}.");
builder.Entity<T>().Ignore(prop.Name);
}
}
}
}
The ITokenAccessor provides access to the jwt access token of the current user.
The above code does not work because OnModelCreating is executed before a user is logged in and that's the reason why _tokenAccessor.UserRoles is always empty at this point.
Is there a way to achieve the property/column based access in entity framework core? I spent hours researching but unfortunately couldn't find a solution. And I am very surprised that property/column based access should be such an unusual requirement.
Related
We have a database first approach using EF.
We use EntityTypeConfiguration<T> to configure the mapping where T is the entity.
We often do things like Property(t => t.EntityType).HasColumnName("EntityType_ID"); What I wish is to have an extension method on the return type of Property(), which is PrimitivePropertyConfiguration that does it for me. Like so.
public static class EntityTypeConfigurationExtension
{
public static PrimitivePropertyConfiguration IsForeignKey(this PrimitivePropertyConfiguration propertyConfiguration)
{
Type typeOfEntity; //The problem is, how to know this type without passing it to this method?
return propertyConfiguration.HasColumnName(typeOfEntity.Name + "_ID");
}
}
The problem is, as you can see in the commented line, I do not see a way to get the type of the property being configured. How can I get the type of property being configured?
If you need to add "_ID" to all your foreign keys you could make your own custom convention. Like this:
public class SuffixForeignKeyNameConvention : IStoreModelConvention<AssociationType>
{
public SuffixForeignKeyNameConvention()
{
}
public void Apply(AssociationType association, DbModel model)
{
if (association.IsForeignKey)
{
AddSuffix(association.Constraint.ToProperties);
}
}
private void AddSuffix(IEnumerable<EdmProperty> properties)
{
string result;
foreach (var property in properties)
{
result = property.Name;
property.Name = $"{result}_ID";
}
}
}
And then apply it:
modelBuilder.Conventions.Add(new SuffixForeignKeyNameConvention());
After using this convention all your foreign keys should have _ID suffix. Hope it helps.
I work on a framework with EF. I want to get all ignored properties of an entity to build some special queries. How can I do it?
public class Customer
{
public int Id { get; set; }
public DateTime BirthDate { get; set; }
public int Age { get; set; }
}
public class CustomerContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().Ignore(customer => customer.Age);
base.OnModelCreating(modelBuilder);
}
public DbSet<Customer> Customers { get; set; }
}
public static class DbContextExtensions
{
public static List<string> GetIgnoredProperties(this DbContext context, string entityTypeName)
{
// ???
}
}
I know this is not answering your original question, and in my comments I mentioned that you should use reflection, but that was only because I read your question wrong.
Here is an alternative using reflection, for if you do not come right.
If you assign the [NotMapped] attribute to the properties on your class that you would like to ignore, you could possibly retrieve all [NotMapped] properties using reflection. Below is an example of how this could be achieved.
var resultArray = yourClassInstance.GetType().GetProperties()
.Where(prop => Attribute.IsDefined(prop, typeof(NotMappedAttribute)));
Hope this helps you in some way.
You can achieve what you want by calling the DbModelBuilder.Build. It will create a DbModel base on configuration setup by the DbModelBuilder. The DbModel expose a ConceptualModel that hold the types used by the context. The EdmModel hold each type that are declared in the context, and for each type, it hold the properties that has not been ignored by the DbModelBuilder during it's configuration. So, to achieve what you want, you have to intersect the properties of each entity type with those present in the EdmModel. It will give the delta between them, thefore the ignored properties. Here an example :
public class CustomerContext : DbContext
{
private static IReadOnlyDictionary<Type, IReadOnlyCollection<PropertyInfo>> _ignoredProperties;
/// Hold the ignored properties configured from fluent mapping
public static IReadOnlyDictionary<Type, IReadOnlyCollection<PropertyInfo>> IgnoredProperties
{
get
{
return _ignoredProperties;
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().Ignore(customer => customer.Age);
// Build ignored properties only if they are not
if (_ignoredProperties == null)
{
var model = modelBuilder.Build(this.Database.Connection);
var mappedEntityTypes = new Dictionary<Type, IReadOnlyCollection<PropertyInfo>>();
foreach (var entityType in model.ConceptualModel.EntityTypes)
{
var type = Type.GetType(entityType.FullName);
var typeProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var mappedProperties = entityType.DeclaredProperties.Select(t => t.Name)
.Union(entityType.NavigationProperties.Select(t => t.Name));
mappedEntityTypes.Add(type, new ReadOnlyCollection<PropertyInfo>(
typeProperties.Where(t => !mappedProperties.Contains(t.Name)).ToList()));
}
_ignoredProperties = new ReadOnlyDictionary<Type, IReadOnlyCollection<PropertyInfo>>(mappedEntityTypes);
}
base.OnModelCreating(modelBuilder);
}
public DbSet<Customer> Customers { get; set; }
}
The IgnoreProperties property is a singleton that will be initialized the first time you will use the context. It will be null before that, so will have to ensure that nothing use it until it's initialized. It's readonly, so you don't have to worrie about accidental clear of the collection. The entity type is used as key, and the value expose a collection that hold ignored properties. Example of use :
var properties = CustomerContext.IgnoredProperties[typeof(Customer)];
Cons :
With this approach is that the DbModel will be built twice, one time to gather the ignored properties, and second time by EntityFramework when the DbCompiledModel will be cached for futur ObjectContext creation. It can have an impact on the cold start of the DbContext, it means that the fist time you will execute a query over your context, it will be a bit slower. It will depend on the size of the DbContext. Warm queries should not suffer. OnModelCreating will be called once anyway.
Pros :
All changes made on de DbModelBuilder configuration will be automatically reflected in the IgnoredProperties property.
This is the error I'm receiving:
Message = "Unable to cast the type 'App.Models.Subject' to type
'App.Context.ITenantData'. LINQ to Entities only supports casting EDM
primitive or enumeration types."
In an attempt to implement multi-tenancy in my application, I added a Tenants table and linked every tenant-specific model to a Tenant (including Subjects).
I got a lot of help from this post: DbSet, ModelBuilder, and EF Navigation Properties
But now I'm stuck with the above casting issue.
My TenantContext:
public class TenantContext : DbContext {
private readonly RealContext _realContext;
private readonly Tenant _tenant;
public TenantContext(Tenant tenant)
: base("name=DefaultConnection") {
this._tenant = tenant;
this._realContext = new RealContext();
}
// _realContext.Subjects is a DbSet
public IQueryable<Subject> Subjects { get { return FilterTenant(_realContext.Subjects); } }
private IQueryable<T> FilterTenant<T>(IQueryable<T> values) where T : ITenantData
{
return values.Where(x => x.TenantId == _tenant.TenantId);
}
}
With ITenantData:
public interface ITenantData {
int TenantId { get; set; }
}
And Subject implements ITenantData with a TenantId property:
[ForeignKey("Tenant")]
public int TenantId { get; set; }
Now, when I query using TenantContext, I get the above error:
using (var db = CreateContext()) { // returns tenantContext
var dbSubjects = db.Subjects;
var subjects = dbSubjects.ToList(); // error occurs here
What am I doing wrong?
Also - I'm pretty new to this, so if I'm missing anything critical here, let me know and I'll post up. Thank you for any help you can provide.
Updating my TenantContext to include class fixed the problem, but I don't know why:
private IQueryable<T> FilterTenant<T>(IQueryable<T> values) where T : class, ITenantData
{
return values.Where(x => x.TenantId == _tenant.TenantId);
}
If anyone wants to write up anything about the reasoning behind this, I'll gladly accept your answer.
Here:
values.Where(x => x.TenantId == _tenant.TenantId);
translator doesnt have an idea how to translate _tenant.TenantId into SQL
i know you can define the entity's schema name per class by using ToTable("TableName", "SchemaName") but is there a way to set it up so you can set the schema name for all tables in the configuration as i am getting some weird results when i am using some types of relationship mapping and split entity mapping where it is reverting back to the default dbo.TableName in the internal sql queries
see this earlier post for sql output example
Im having Oracle database-first with EF 4.1, all mappings done with Data Annotations. Different Schema names in Test and Production environments. My solution is to map the Schema dynamically during OnModelCreating with some help of fluent API, reflection and late binding. Iterate through all Context class properties of generic type and do the dirty work. Works for me so far.
public class Context : DbContext
{
public Context()
: base(new OracleConnection(ConfigurationManager.ConnectionStrings["OraAspNetConString"].ConnectionString), true)
{
}
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
foreach (var p in typeof(Context).GetProperties().Where(foo=>foo.PropertyType.IsGenericType))
{
// this is what we are trying to accomplish here --
//modelBuilder.Entity<User>().ToTable("TBL_USERS", "TestSchema");
Type tParam = p.PropertyType.GetGenericArguments()[0]; // typeof User
MethodInfo generic = typeof(DbModelBuilder).GetMethod("Entity").MakeGenericMethod(new[] { tParam });
object entityTypeConfig = generic.Invoke(modelBuilder, null);
MethodInfo methodToTable = typeof(EntityTypeConfiguration<>).MakeGenericType(new[] { tParam }).GetMethod("ToTable", new Type[] { typeof(string), typeof(string) });
methodToTable.Invoke(entityTypeConfig, new[] { GetMappedTableName(tParam), currentOraSchemaName });
}
base.OnModelCreating(modelBuilder);
}
private string currentOraSchemaName = ConfigurationManager.AppSettings.Get("OraSchemaName");
private string GetMappedTableName(Type tParam)
{
TableAttribute tableAttribute = (TableAttribute)tParam.GetCustomAttributes(typeof(TableAttribute), false).FirstOrDefault();
return tableAttribute.Name;
}
}
The user class here, with no hard-coded schema mapping --
[Table("TBL_USERS")]
public class User
{
[Column("USER_ID")]
public string UserId { get; set; }
[Column("USER_NAME")]
public string Name { get; set; }}
Since final EFv4.1 version doesn't have public API for custom conventions you cannot change the schema globally from the API.
i defined an entity called Variable and derived classes by using Table Per Hierarchy (TPH). The Base class "Variable" contains a collection of PropertyValues:
private ICollection<PropertyValue> propertyValues;
public const string DiscriminatorColumn = "Discriminator";
public const string Table = "Variables";
public VariableType VariableType { get; set; }
public string Name { get; set; }
[NotMapped]
public string Discriminator { get; set; }
public virtual ICollection<PropertyValue> PropertyValues
{
get { return this.propertyValues ?? (this.propertyValues = new ObservableCollection<PropertyValue>()); }
set { SetProperty(ref this.propertyValues, value, () => PropertyValues); }
}
Now, i want to derive a SpecialVariable class (or more than one), which define some SpecialProperties (e.g. HighLimit) which should be mapped to an entry in the PropertyValues (table).
public class MySpecialVariabe : Variable
{
public double HighLimit { get; set; }
}
My OnModelCreating function looks currently like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Variable>().HasKey(x => new { x.Id });
modelBuilder.Entity<Variable>()
.Map<MySpecialVariabe>(m => m.Requires(Variable.DiscriminatorColumn).HasValue(typeof(MySpecialVariabe).Name))
.Map<MySpecialVariabe2>(m => m.Requires(Variable.DiscriminatorColumn).HasValue(typeof(MySpecialVariabe2).Name)).ToTable(Variable.Table);
}
Can someone give me some tips how to realize this, without writing tons of bad looking code in the derived class. (Performance is not that important.)
best regards,
Chris
You can't map properties to records. That is how I understand your question. You have some PropertyValues table which is most probably some Key/Value pair and you want to map entity properties as records (data) to this table. This is not something which EF do for you. You must provide not mapped properties which will work with correct record in propertyValues collection.
Something like:
[NotMapped]
public double HighLimit
{
get
{
var current = propertyValues.SingleOrDefault(p => p.Key == "HighLimit");
return current != null ? current.Value : 0.0;
}
set
{
var current = propertyValues.SingleOrDefault(p => p.Key == "HighLimit");
if (current != null)
{
current.Value = value;
}
else
{
propertyValues.Add(new PropertyValue { Key = "HighLimit", Value = value });
}
}
}
The problem with this approach is that you can't use HighLimit in Linq-to-entities queries - you must always use PropertyValues.
Moreover TPH in EF requires that properties of derived entity (MySpecialVariable) are mapped to the same table as parent entity (Variable). You can't map properties of derived entity into data stored in other table (PropertyValues).