Calculate NotMapped property when loading from EF Core - c#

We do have an entity class defined as below:
[Table("Users", Schema = "Mstr")]
[Audited]
public class User
{
public virtual string FamilyName { get; set; }
public virtual string SurName { get; set; }
[NotMapped]
public virtual string DisplayName
{
get => SurName + " " + FamilyName;
private set { }
}
}
This is working just fine. Now we would like to extract the logic part SurName + " " + FamilyName to a helper class which is usually injected with dependency injection. Unfortunately DI is not working for an entity class.
Therefor my question: is there any way to intercept the creation of new User objects? Is there a method from EF which I could override to execute some additional logic after a User object was created by EF?

Actually (at least in EF Core 6) you can use DI when constructing entities. Solution is a little bit hacky and based on the EF Core capability to inject "native" services like the context itself into entities constructors:
Currently, only services known by EF Core can be injected. Support for injecting application services is being considered for a future release.
And AccessorExtensions.GetService<TService> extension method which seems to support resolving services from DI.
So basically just introduce ctor accepting your DbContext as a parameter to the entity and call GetService on it and use service:
public class MyEntity
{
public MyEntity()
{
}
public MyEntity(SomeContext context)
{
var valueProvider = context.GetService<IValueProvider>();
NotMapped = valueProvider.GetValue();
}
public int Id { get; set; }
[NotMapped]
public string NotMapped { get; set; }
}
// Example value provider:
public interface IValueProvider
{
string GetValue();
}
class ValueProvider : IValueProvider
{
public string GetValue() => "From DI";
}
Example context:
public class SomeContext : DbContext
{
public SomeContext(DbContextOptions<SomeContext> options) : base(options)
{
}
public DbSet<MyEntity> Entities { get; set; }
}
And example:
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IValueProvider, ValueProvider>();
serviceCollection.AddDbContext<SomeContext>(builder =>
builder.UseSqlite($"Filename={nameof(SomeContext)}.db"));
var serviceProvider = serviceCollection.BuildServiceProvider();
// init db and add one item
using (var scope = serviceProvider.CreateScope())
{
var someContext = scope.ServiceProvider.GetRequiredService<SomeContext>();
someContext.Database.EnsureDeleted();
someContext.Database.EnsureCreated();
someContext.Add(new MyEntity());
someContext.SaveChanges();
}
// check that value provider is used
using (var scope = serviceProvider.CreateScope())
{
var someContext = scope.ServiceProvider.GetRequiredService<SomeContext>();
var myEntities = someContext.Entities.ToList();
Console.WriteLine(myEntities.First().NotMapped); // prints "From DI"
}
Note that var valueProvider = context.GetService<IValueProvider>(); will throw if service is not registered so possibly next implementation is better:
public MyEntity(SomeContext context)
{
var serviceProvider = context.GetService<IServiceProvider>();
var valueProvider = serviceProvider.GetService<IValueProvider>();
NotMapped = valueProvider?.GetValue() ?? "No Provider";
}
Also you can consider removing not mapped property and creating separate model with it and service which will perform the mapping.
Also in 7th version of EF Core a new hook for exactly this case should be added. See this github issue.
UPD. EF Core 7 approach.
EF 7 adds IMaterializationInterceptor (and bunch of others - see the docs) which can be used exactly for this goal. So updated code can look like the following:
No need for ctor accepting context in entity:
public class MyEntity
{
public int Id { get; set; }
[NotMapped]
public string NotMapped { get; set; }
}
Create an interceptor and overload one of it's methods (I went with InitializedInstance):
class NotMappedValueGeneratingInterceptor : IMaterializationInterceptor
{
public static NotMappedValueGeneratingInterceptor Instance = new ();
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is MyEntity my)
{
var valueProvider = materializationData.Context.GetService<IValueProvider>();
my.NotMapped = valueProvider.GetValue();
}
return entity;
}
}
And add interceptor to the context setup, with our DI approach AddDbContext changes to:
serviceCollection.AddDbContext<SomeContext>(builder =>
builder.UseSqlite($"Filename={nameof(SomeContext)}.db")
.AddInterceptors(NotMappedValueGeneratingInterceptor.Instance));

In your DbContext or whatever your context file is called you can intercept the SaveChanges() method and override it with your own things. In my example I override SaveChanges() to automatically add my audit fields so I don't have to duplicate it all over the code in a million places.
here is my example. So when a new object is being created you can override it. In My example I override both New records Added and Records modified.
These are notated at EntitState.Added and EntityStateModified.
Here is the code.
public override int SaveChanges()
{
var state = this.ChangeTracker.Entries().Select(x => x.State).ToList();
state.ForEach(x => {
if (x == EntityState.Added)
{
//Create new record changes
var created = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Added).Select(e => e.Entity).ToArray();
foreach (var entity in created)
{
if (entity is AuditFields)
{
var auditFields = entity as AuditFields;
auditFields.CreateDateTimeUtc = DateTime.UtcNow;
auditFields.ModifiedDateTimeUtc = DateTime.UtcNow;
auditFields.Active = true;
}
}
}
else if (x == EntityState.Modified)
{
//Modified record changes
var modified = this.ChangeTracker.Entries().Where(e => e.State == EntityState.Modified).Select(e => e.Entity).ToArray();
foreach (var entity in modified)
{
if (entity is AuditFields)
{
var auditFields = entity as AuditFields;
auditFields.ModifiedDateTimeUtc = DateTime.UtcNow;
}
}
}
else
{
//do nothing
}
});
return base.SaveChanges();
}
Since you said:
is there any way to intercept the creation of new User objects?
You would want to do your logic in the EntityState.Added area of code above and this will allow you to intercept the creation of your new User and do whatever you want to do before it is saved to Database.

Related

EntityFramework core not loading child entities after disposing the DbContext (and recreating it)

We're using EF core and I just learned that using the DbContext as long lived object is not a good idea. The DbContext is not designed to be a long lived object.
So I now inject a new instance for every transaction, which I thought worked great. But now I see that child entities are not being populated after disposing the DbContext and creating a new one.
Some example code to show the problem
public InitialTestData(Func<IMachineRepository> machineRepositoryFactory)
{
{
using var machineRepository = machineRepositoryFactory();
if (!machineRepository.FindAll().Any())
{
machineRepository.Add("411-01", "https://localhost:5002/411-01");
machineRepository.Add("411-02", "https://localhost:5002/411-02");
machineRepository.Add("411-03", "https://localhost:5002/411-03");
machineRepository.Add("413-01", "https://localhost:5002/413-01");
machineRepository.Add("413-02", "https://localhost:5002/413-02");
machineRepository.Add("413-03", "https://localhost:5002/413-03");
machineRepository.Save();
foreach (var machineEntity in machineRepository.FindAll())
{
var machineStatusChangeEntity = new MachineStatusChange
{
DateTime = DateTime.Now,
State = MachineStateDataModel.Idle
};
machineEntity.StatusChanges.Add(machineStatusChangeEntity);
machineRepository.Save();
}
}
var bla = machineRepository.FindAll().ToList();
foreach (var machine in bla)
{
// machine entity has a list of one child entity as expected
}
}
{
using var machineRepository = machineRepositoryFactory();
var bla = machineRepository.FindAll().ToList();
foreach (var machine in bla)
{
// machine entity has zero child entities, why? it's present in the database
}
}
}
In the above example I (autofac) creates the machine repository twice. Each repository gets a DbContext injected. The first time, I add some entities and save the changes (persisted in database successfully). I can (obviously) still query on this repository, as everything is still cached as well.
But when I then recreate the machine repository for the second time, it is able to query the Machine entities (the "parent" entities) but it no longer is able to find the related child elements. As mentioned, they are stored in the database, I double checked the Guids of parent and childs, the all match perfectly. Yet EF code doesn't "remake" the relation somehow.
Am I missing something important in the OnModelConfiguring (posted below) or did I design the entities wrong?
See below the remaining classes that make the "database layer"
Repository pattern:
public class MachineRepository : RepositoryBase<Machine>, IMachineRepository
{
public MachineRepository(PatDatabase repositoryContext) : base(repositoryContext)
{
}
}
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
protected PatDatabase RepositoryContext { get; set; }
protected RepositoryBase(PatDatabase repositoryContext)
{
RepositoryContext = repositoryContext;
}
public IQueryable<T> FindAll() => RepositoryContext.Set<T>();
public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression) =>
RepositoryContext.Set<T>().Where(expression);
public EntityEntry<T> Refresh(T entity) => RepositoryContext.Entry(entity);
public void Create(T entity) => RepositoryContext.Set<T>().Add(entity);
public void Update(T entity) => RepositoryContext.Set<T>().Update(entity);
public void Delete(T entity) => RepositoryContext.Set<T>().Remove(entity);
}
The two entity classes
[Table("Machine")]
public class Machine
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string BaseUrl { get; set; }
public ICollection<MachineStatusChange> StatusChanges { get; set; } = new List<MachineStatusChange>();
}
[Table("MachineStatusChange")]
public record MachineStatusChange
{
[Key]
public Guid Id { get; set; }
[ForeignKey(nameof(Machine))]
public Guid MachineId { get; set; }
public DateTime DateTime { get; set; }
public MachineStateDataModel State { get; set; }
public Machine Machine { get; set; }
}
The "PatDatabase" / DbContext
public class PatDatabase : DbContext
{
private readonly string _connectionString;
// TODO, discuss/investigate
// Any purpose to define DbSet<T> as they did in tutorial?
//public DbSet<Machine> Machines { get; set; }
/// <summary>
/// Used by dotnet ef migrations
/// NOTE: Autofac will always prefer the constructor with most arguments
/// </summary>
public PatDatabase()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("IAI.ToolSuite.PAT.Server.appsettings.json");
var configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();
// TODO BP: replace "LocalMySqlConnection" with "MySqlConnection" when figured out migrations in docker properly
// It seems that 'dotnet ef migrations remove' requires access to the database(??)
// In my case, my docker-compose is down, I would expect that's not a problem.... To be investigated...
_connectionString = configuration["MySqlConnection:connectionString"];
}
/// <summary>
/// Constructor used by autofac (autofac will always prefer constructor with most arguments if resolvable)
/// </summary>
public PatDatabase(IConfiguration configuration, IWebHostEnvironment env)
{
if (env.IsLocal())
{
_connectionString = configuration["MySqlConnection:connectionString"];
}
else
{
_connectionString = configuration["MySqlConnection:connectionString"];
}
}
/// <summary>
/// IMPORTANT
/// override the OnConfiguring allows us to keep a parameterless constructor
/// ef migrations tool requires a parameterless constructor
/// </summary>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql(_connectionString, MySqlServerVersion.LatestSupportedServerVersion,
builder =>
{
builder.EnableRetryOnFailure(20, TimeSpan.FromSeconds(10), null);
});
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Machine>().HasMany(m => m.StatusChanges);
modelBuilder.Entity<MachineStatusChange>()
.HasOne(m => m.Machine)
.WithMany(m => m.StatusChanges);
}
public void MigrateIfNeeded()
{
if (Database.GetPendingMigrations().Any())
{
Database.Migrate();
}
// I think calling ensure created before migrations can cause issues
// when migrations exist but the database or tables were dropped
Database.EnsureCreated();
}
}
Argh... I wasn't including the child entities at all after disposing it...
My apologies for wasting anybodies time.
The obvious fix
var bla = machineRepository.FindAll().Include(m => m.StatusChanges).ToList();
My actual problem which I had was this
using var machineRepository = _machineRepositoryFactory();
var machines = machineRepository
.FindAll();
// Here, it would've had been a GREAT idea to
// capture the resulting IQueryable return by `Include`
machines
.Include(m => m.StatusChanges)
// Iterating over machines now, will obviously not include the child entities.
So the fix...
machine = machines
.Include(m => m.StatusChanges)
Or even simpler of course...
using var machineRepository = _machineRepositoryFactory();
var machines = machineRepository
.FindAll()
.Include(m => m.StatusChanges);

Lazy Loading for Owned Types

I am moving my first steps towards Domain Driven Design using Entity Framework Core. I have a User entity that, in a simplified version, has only Id and ProfilePhoto. However, I want to store profile photos in a different table, that is why I created an Owned Type containing the profile photo and configured in this way:
User:
public class User
{
private int id;
public int Id => this.id;
//private UserProfilePhoto userProfilePhoto;
public virtual UserProfilePhoto UserProfilePhoto { get; set; }
private User()
{
}
public static User Create(byte[] profilePhoto)
{
var user = new User();
user.UserProfilePhoto = new UserProfilePhoto(profilePhoto);
return user;
}
public void SetProfilePhoto(byte[] profilePhoto)
{
this.UserProfilePhoto = new UserProfilePhoto(profilePhoto);
}
}
UserProfilePhoto:
public class UserProfilePhoto
{
public byte[] ProfilePhoto { get; private set; }
public UserProfilePhoto(byte[] profilePhoto)
{
this.ProfilePhoto = profilePhoto;
}
}
DbContext configuration:
public class ModelContext : DbContext
{
public ModelContext(DbContextOptions<ModelContext> options) : base(options)
{
}
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
OnUserModelCreating(modelBuilder);
}
protected void OnUserModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasKey(u => u.Id);
modelBuilder.Entity<User>()
.Property(u => u.Id)
.HasField("id");
modelBuilder.Entity<User>()
.OwnsOne(u => u.UserProfilePhoto, builder =>
{
builder.ToTable("UserProfilePhoto");
builder.Property(u => u.ProfilePhoto)
.IsRequired();
});
}
}
I chose to use an Owned type because I want the profile photo to be accessible only from the user entity. with a one-to-one mapping, I could still access the UserProfilePhoto table using context.Set<UserProfilePhoto>() for example and, for what I read about DDD aggregates, this could mean skipping User business logic.
So, I migrated and the database model is just like I expected it to be: the UserProfilePhoto table with a primary and foreign key to User.Id.
Obviously in my queries I do not want to load the entire User entity every time, so I enabled Lazy Loading, unsuccessfully. This is the code I tried in a unit test:
protected ModelContext GetModelContext(DbContextOptionsBuilder<ModelContext> builder)
{
builder
.UseLoggerFactory(loggerFactory)
.UseLazyLoadingProxies()
.EnableDetailedErrors();
var ctx = new ModelContext(builder.Options);
ctx.Database.EnsureCreated();
return ctx;
}
[TestMethod]
public async Task TestMethod1()
{
var builder = new DbContextOptionsBuilder<ModelContext>()
.UseSqlServer(...);
var ctx = this.GetModelContext(builder);
var user = User.Create(new byte[] { });
try
{
await ctx.Users.AddAsync(user);
await ctx.SaveChangesAsync();
var users = ctx.Users;
foreach (var u in users)
{
Console.WriteLine(u.Id);
}
}
finally
{
ctx.Users.Remove(user);
await ctx.SaveChangesAsync();
ctx.Database.EnsureDeleted();
}
}
And here the SQL generated:
SELECT [u].[Id], [u0].[UserId], [u0].[ProfilePhoto]
FROM [Users] AS [u]
LEFT JOIN [UserProfilePhoto] AS [u0] ON [u].[Id] = [u0].[UserId]
I do not exactly know if it works, but injecting an ILazyLoader is not an solution for me, on the other hand, it feels like dirtying the model.
My doubt is that Owned types do not bind to the principal entity through actual navigation properties, so creating proxy for them is not supported.
What is wrong with my approach? Is it DDD? And if so, how can I lazily load owned entities?
I found an issue on Github related to this, although it does not answer my question.
Edit
my goal is to prevent the access to the UserProfilePhoto table from EF api (See comments). If I managed to do this, then protecting my UserProfilePhoto class and encapsulating it in the User class would be easy, something like this:
User
...
protected virtual UserProfilePhoto UserProfilePhoto { get; set; }
public void SetProfilePhoto(byte[] profilePhoto)
{
this.UserProfilePhoto.SetProfilePhoto(profilePhoto);
}
public byte[] GetProfilePhoto()
{
return this.UserProfilePhoto.ProfilePhoto;
}
...
I tried this code with a one-to-one mapping and works perfectly, even with lazy loading. How could I do this with only Owned Types? are there other ways?
EF Core loads owned types automatically when the owner gets loaded (from Owned Entity Types: Querying owned types)
When querying the owner the owned types will be included by default. It is not necessary to use the Include method, even if the owned types are stored in a separate table.
Therefore using owned types does not fulfill your requirement of being loaded only on demand.
(You can tinker with Metadata.PrincipalToDependent.SetIsEagerLoaded(false) etc., but this is very much unsupported, unlikely to work in all cases and could break any time.)
Options without using owned types (in order of recommendation)
Override DbContext.Set<>(), DbContext.Find() etc. and throw if called inappropriately
Implement a traditional custom Unit-of-Work and Repository pattern, that gives you full control over the API exposed (trades flexibility for control)
Add an expression visitor early to the query pipeline (register IQueryTranslationPreprocessorFactory and derive from RelationalQueryTranslationPreprocessorFactory), that throws if a DbSet<UserProfilePhoto> is used anywhere in the query
Provide your own IDbSetSource (and InternalDbSet) implementation (both internal) and throw if called inappropriately
Overriding DbContext methods
Generally, just overriding DbContext.Set<>(), DbContext.Find() etc. should be the simplest solution. You could decorate the types that you don't want to be queried directly with a custom attribute and then simply just check, that TEntity etc. has not have been decorated with this custom attribute.
For easier maintainability, all the overridden methods can be moved to a base class, that can also perform some runtime check to ensure, that all methods in question have been overridden (of course those checks could also be done by a unit test).
Here is a sample demonstrating this approach:
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class DontRootQueryMeAttribute : Attribute
{
}
public class User
{
public int Id { get; private set; }
public virtual UserProfilePhoto UserProfilePhoto { get; set; }
public static User Create(byte[] profilePhoto)
{
var user = new User
{
UserProfilePhoto = new UserProfilePhoto(profilePhoto)
};
return user;
}
}
[DontRootQueryMeAttribute]
public class UserProfilePhoto
{
public int Id { get; set; }
public byte[] ProfilePhoto { get; private set; }
public UserProfilePhoto(byte[] profilePhoto)
{
ProfilePhoto = profilePhoto;
}
}
public abstract class ModelContextBase : DbContext
{
private static readonly string[] OverriddenMethodNames =
{
nameof(DbContext.Set),
nameof(DbContext.Query),
nameof(DbContext.Find),
nameof(DbContext.FindAsync),
};
static ModelContextBase()
{
var type = typeof(ModelContextBase);
var overriddenMethods = type
.GetRuntimeMethods()
.Where(
m => m.IsPublic &&
!m.IsStatic &&
OverriddenMethodNames.Contains(m.Name) &&
m.GetRuntimeBaseDefinition() != null)
.Select(m => m.GetRuntimeBaseDefinition())
.ToArray();
var missingOverrides = type.BaseType
.GetRuntimeMethods()
.Where(
m => m.IsPublic &&
!m.IsStatic &&
OverriddenMethodNames.Contains(m.Name) &&
!overriddenMethods.Contains(m))
.ToArray();
if (missingOverrides.Length > 0)
{
throw new InvalidOperationException(
$"The '{nameof(ModelContextBase)}' class is missing overrides for {string.Join(", ", missingOverrides.Select(m => m.Name))}.");
}
}
private void EnsureRootQueryAllowed<TEntity>()
=> EnsureRootQueryAllowed(typeof(TEntity));
private void EnsureRootQueryAllowed(Type type)
{
var rootQueriesAllowed = type.GetCustomAttribute(typeof(DontRootQueryMeAttribute)) == null;
if (!rootQueriesAllowed)
throw new InvalidOperationException($"Directly querying for '{type.Name}' is prohibited.");
}
public override DbSet<TEntity> Set<TEntity>()
{
EnsureRootQueryAllowed<TEntity>();
return base.Set<TEntity>();
}
public override DbQuery<TQuery> Query<TQuery>()
{
EnsureRootQueryAllowed<TQuery>();
return base.Query<TQuery>();
}
public override object Find(Type entityType, params object[] keyValues)
{
EnsureRootQueryAllowed(entityType);
return base.Find(entityType, keyValues);
}
public override ValueTask<object> FindAsync(Type entityType, params object[] keyValues)
{
EnsureRootQueryAllowed(entityType);
return base.FindAsync(entityType, keyValues);
}
public override ValueTask<object> FindAsync(Type entityType, object[] keyValues, CancellationToken cancellationToken)
{
EnsureRootQueryAllowed(entityType);
return base.FindAsync(entityType, keyValues, cancellationToken);
}
public override TEntity Find<TEntity>(params object[] keyValues)
{
EnsureRootQueryAllowed<TEntity>();
return base.Find<TEntity>(keyValues);
}
public override ValueTask<TEntity> FindAsync<TEntity>(params object[] keyValues)
{
EnsureRootQueryAllowed<TEntity>();
return base.FindAsync<TEntity>(keyValues);
}
public override ValueTask<TEntity> FindAsync<TEntity>(object[] keyValues, CancellationToken cancellationToken)
{
EnsureRootQueryAllowed<TEntity>();
return base.FindAsync<TEntity>(keyValues, cancellationToken);
}
// Add other overrides as needed...
}
public class ModelContext : ModelContextBase
{
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
#"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63887500_01")
.UseLoggerFactory(LoggerFactory.Create(b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
OnUserModelCreating(modelBuilder);
}
protected void OnUserModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(
entity =>
{
entity.HasOne(e => e.UserProfilePhoto)
.WithOne()
.HasForeignKey<UserProfilePhoto>(e => e.Id);
});
}
}
internal static class Program
{
private static void Main()
{
var accessingSetThrows = false;
using (var ctx = new ModelContext())
{
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
var user = User.Create(new byte[] { });
ctx.Users.Add(user);
ctx.SaveChanges();
// Make sure, that UserProfilePhoto cannot be queried directly.
try
{
ctx.Set<UserProfilePhoto>()
.ToList();
}
catch (InvalidOperationException)
{
accessingSetThrows = true;
}
Debug.Assert(accessingSetThrows);
}
// No eager loading by default with owned type here.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto == null);
}
// Explicitly load profile photo.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
ctx.Entry(users[0]).Reference(u => u.UserProfilePhoto).Load();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto != null);
}
}
}
}
Providing an IQueryTranslationPreprocessorFactory implementation
An expression visitor can be used to solve the issue by using an IQueryTranslationPreprocessorFactory implementation to search the query for a specific expression, that is only added when the new InternalQuery() extension method is called and throwing, if it is missing and a non-root entity is being queried. In practice, this should be good enough to make sure, that nobody in the team queries non-root objects by accident.
(You could also add an internal class instance as a constant parameter to the method call expression, that is then evaluated later in the expression visitor to ensure, that the caller really had internal access to the InternalQuery() methods. But this is just icing on the cake and unnecessary in practice, since developers could use reflection to bypass any access restrictions anyway. So I wouldn't bother to implement this.)
Here it the implementation (using a custom interface instead of a custom attribute to mark entities that should not be queried directly):
using System;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
#region Models
public class User
{
public int Id { get; private set; }
public virtual UserProfilePhoto UserProfilePhoto { get; set; }
public static User Create(byte[] profilePhoto)
{
var user = new User
{
UserProfilePhoto = new UserProfilePhoto(profilePhoto)
};
return user;
}
}
public class UserProfilePhoto : INonRootQueryable
{
public int Id { get; set; }
public byte[] ProfilePhoto { get; private set; }
public UserProfilePhoto(byte[] profilePhoto)
{
ProfilePhoto = profilePhoto;
}
}
#endregion
#region Custom implementations
public interface INonRootQueryable
{
}
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 ThrowOnNoneRootQueryableViolationExpressionVisitor().Visit(query);
return base.Process(query);
}
}
public class ThrowOnNoneRootQueryableViolationExpressionVisitor : ExpressionVisitor
{
private bool _isInternalQuery;
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.GetGenericMethodDefinition() == CustomQueryableExtensions.InternalQueryMethodInfo)
{
_isInternalQuery = true;
return node.Arguments[0];
}
return base.VisitMethodCall(node);
}
protected override Expression VisitConstant(ConstantExpression node)
{
var expression = base.VisitConstant(node);
// Throws if SomeEntity in a DbSet<SomeEntity> implements INonRootQueryable and the query was not chained
// to the `InternalQuery()` extension method.
return !_isInternalQuery &&
node.Type.IsGenericType &&
node.Type.GetGenericTypeDefinition() == typeof(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<>) &&
node.Type.GenericTypeArguments.Length == 1 &&
typeof(INonRootQueryable).IsAssignableFrom(node.Type.GenericTypeArguments[0])
? throw new InvalidOperationException($"Directly querying for '{node.Type.Name}' is prohibited.")
: expression;
}
}
internal static class CustomQueryableExtensions
{
internal static readonly MethodInfo InternalQueryMethodInfo
= typeof(CustomQueryableExtensions)
.GetTypeInfo()
.GetDeclaredMethods(nameof(InternalQuery))
.Single(m => m.GetParameters().Length == 1 &&
m.GetParameters()[0].ParameterType.Namespace == $"{nameof(System)}.{nameof(System.Linq)}" &&
m.GetParameters()[0].ParameterType.Name.StartsWith(nameof(IQueryable)) &&
m.GetParameters()[0].ParameterType.GenericTypeArguments.Length == 1);
internal static IQueryable<TSource> InternalQuery<TSource>(this IQueryable<TSource> source)
=> source.Provider.CreateQuery<TSource>(
Expression.Call(
null,
InternalQueryMethodInfo.MakeGenericMethod(typeof(TSource)),
source.Expression));
internal static IQueryable<TProperty> InternalQuery<TEntity, TProperty>(this ReferenceEntry<TEntity, TProperty> source)
where TEntity : class
where TProperty : class
=> source.Query()
.InternalQuery();
}
#endregion
public class ModelContext : DbContext
{
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Register the custom type IQueryTranslationPreprocessorFactory.
// 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=So63887500_05")
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
OnUserModelCreating(modelBuilder);
}
protected void OnUserModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(
entity =>
{
entity.HasOne(e => e.UserProfilePhoto)
.WithOne()
.HasForeignKey<UserProfilePhoto>(e => e.Id);
});
}
}
internal static class Program
{
private static void Main()
{
var accessingSetThrows = false;
using (var ctx = new ModelContext())
{
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
var user = User.Create(new byte[] { });
ctx.Users.Add(user);
ctx.SaveChanges();
}
// Make sure, that UserProfilePhoto cannot be queried directly by default.
using (var ctx = new ModelContext())
{
try
{
ctx.Set<UserProfilePhoto>()
.ToList();
}
catch (InvalidOperationException)
{
accessingSetThrows = true;
}
Debug.Assert(accessingSetThrows);
}
// Make sure, that UserProfilePhoto can be queried directly, when using the `InternalQuery()` extension
// method.
using (var ctx = new ModelContext())
{
var userProfilePhotos = ctx.Set<UserProfilePhoto>()
.InternalQuery()
.ToList();
Debug.Assert(userProfilePhotos.Count == 1);
}
// No eager loading of referenced types by default.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto == null);
}
// Eager loading of referenced types is allowed, when using the `InternalQuery()` extension method.
using (var ctx = new ModelContext())
{
var users = ctx.Users
.Include(u => u.UserProfilePhoto)
.InternalQuery()
.ToList();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto != null);
}
// Explicitly load profile photo, when using the `InternalQuery()` extension method.
using (var ctx = new ModelContext())
{
var users = ctx.Users.ToList();
ctx.Entry(users[0])
.Reference(u => u.UserProfilePhoto)
.InternalQuery()
.Load();
Debug.Assert(users.Count == 1);
Debug.Assert(users[0].UserProfilePhoto != null);
}
}
}
}
Should bytes really be part of the domain? Do you actually run any business logic on those bytes in the user profile context? Is there really a use case where you'd want to access the bytes from within the User AR?
If not then perhaps it makes more sense decoupling the bytes storage from the photo's metadata and introduce a ProfilePhoto VO with a storageUrl/storageId property to locate the bytes.
Don't forget that your domain model should be designed for commands, not queries & the presentation layer.
Granted, now you can't easily have ACID properties when storing the bytes & the AR's data in the DB, but it's usually easy to cope with that with a cleanup process.
If you don't need profile photo's metadata in User to enforce business rules then you may also consider making ProfilePhoto it's own AR.
Finally, I think trying to prevent ORM misuse is unnecessary. The ORM should be seen as a low-level API which shouldn't ever be used directly to change AR states. I think it's safe to assume developers will have enough rigour to respect that rule just like they should respect the overall system's architecture. If they don't you have bigger problems. If it was as easy as adding a private modifier to a member then sure, but it seems to be needing a lot of efforts so I'd just go the pragmatic way...
I found a temporary solution:
modelBuilder.Entity<User>()
.OwnsOne(u => u.UserProfilePhoto, builder =>
{
builder.Metadata.IsOwnership = false;
builder.Metadata.IsRequired = false;
builder.Metadata.PrincipalToDependent.SetIsEagerLoaded(false);
builder.ToTable("UserProfilePhoto");
builder.Property(u => u.ProfilePhoto)
.IsRequired();
});
I do not like it and I guess EF allows you to configure that in other, more clear, ways. I am not accepting this answer, hoping someone else could point me in the right direction.
EDIT: proxy works this way but when a User is deleted, the association with the UserProfilePhoto is severed:
The association between entities 'User' and 'UserProfilePhoto' with
the key value '{UserId: 1}' has been severed but the relationship is
either marked as 'Required' or is implicitly required because the
foreign key is not nullable. If the dependent/child entity should be
deleted when a required relationship is severed, then setup the
relationship to use cascade deletes.
I even tried to specify through metadata the DeleteBehaviour.Cascade option but it probably breaks an internal constraint.
Moreover, it is now accessible via DbContext.Set<UserProfilephoto>(), which is not what I want.

EF Core configuration problem with owned type used in 2 different classes

I'm using entity framework core and I would like to use the same owned type in 2 different classes. This is normally fine however in my case I am getting an error.
I am using a MySql database and the requirement is that all booleans are mapped to a field in the database with column type tinyint(1). To achieve this in my OnModelCreating method I loop through all the properties and if the property is boolean I map it to tinyint(1). However as soon as I use the same owned type in 2 different classes I get the error.
Below I have written a demo program which shows my problem. All you need to recreate this is 2 tables, organisations and contacts. Both with fields id, street and home. To use MySQL I have installed the nuget package MySql.Data.EntityFrameworkCore (v8.0.17). I've run the code in a .net core 2.2 console app.
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace MyDemo
{
class Program
{
static void Main(string[] args)
{
using(var ctx = new MyDbContext())
{
var contact = new Contact
{
Address = new Address
{
Street = "x",
Home = true
}
};
ctx.Contacts.Add(contact);
ctx.SaveChanges();
}
}
}
public class MyDbContext: DbContext
{
public MyDbContext()
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySQL("{my connection string}");
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>()
.OwnsOne(p => p.Address,
a =>
{
a.Property(p => p.Street)
.HasColumnName("street")
.HasDefaultValue("");
a.Property(p => p.Home)
.HasColumnName("home")
.HasDefaultValue(false);
});
modelBuilder.Entity<Organisation>()
.OwnsOne(p => p.Address,
a =>
{
a.Property(p => p.Street)
.HasColumnName("street")
.HasDefaultValue("");
a.Property(p => p.Home)
.HasColumnName("home")
.HasDefaultValue(false);
});
var entityTypes = modelBuilder.Model.GetEntityTypes()
.ToList();
foreach (var entityType in entityTypes)
{
var properties = entityType
.GetProperties()
.ToList();
foreach (var property in properties)
{
if (property.PropertyInfo == null)
{
continue;
}
if (property.PropertyInfo.PropertyType.IsBoolean())
{
modelBuilder.Entity(entityType.ClrType)
.Property(property.Name)
.HasConversion(new BoolToZeroOneConverter<short>())
.HasColumnType("tinyint(1)");
}
}
}
base.OnModelCreating(modelBuilder);
}
public DbSet<Contact>Contacts { get; set; }
public DbSet<Organisation>Organisations { get; set; }
}
public class Contact
{
public int Id { get; set; }
public Address Address { get; set; }
//other contact fields
}
public class Organisation
{
public int Id { get; set; }
public Address Address { get; set; }
//other organisation fields
}
public class Address
{
public string Street { get; set; }
public bool Home{ get; set; }
}
public static class TypeExtensions
{
public static bool IsBoolean(this Type type)
{
Type t = Nullable.GetUnderlyingType(type) ?? type;
return t == typeof(bool);
}
}
}
After running the above code the error message that shows up is System.InvalidOperationException: 'The entity type 'Address' cannot be added to the model because a weak entity type with the same name already exists'. The part of the code that throws the error is this bit
if (property.PropertyInfo.PropertyType.IsBoolean())
{
modelBuilder.Entity(entityType.ClrType)
.Property(property.Name)
.HasConversion(new BoolToZeroOneConverter<short>())
.HasColumnType("tinyint(1)");
}
How can I change my code so that the OnModelCreating method runs without error so that the contact record is saved correctly to the database?
Update (EF Core 3.x):
Still no public way to get EntityTypeBuilder, but at least the constructor argument has been modified to be IMutableEntityType type, so only
using Microsoft.EntityFrameworkCore.Metadata.Builders;
is needed, and the corresponding code now is
var entityTypeBuilder = new EntityTypeBuilder(entityType);
Original (EF Core 2.x):
The problem is that the ClrType is not enough to identify the owned entity type, hence modelBuilder.Entity(Type) cannot be used to obtain the EntityTypeBuilder instance needed for fluently configuring the entity properties.
Seems like there is no good public way to do that in EF Core 2.x, so all I can suggest is to use some of the EF Core internals (luckily publicly accessible under the typical internal usage warning).
You'd need the following usings:
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
The first is for EntityTypeBuilder class, the second is for AsEntityType() extension method which gives you access to the internal class implementing the IEntityType, and in particular the Builder property.
The modified code looks like this:
var entityTypes = modelBuilder.Model.GetEntityTypes()
.ToList();
foreach (var entityType in entityTypes)
{
var properties = entityType
.GetProperties()
.ToList();
// (1)
var entityTypeBuilder = new EntityTypeBuilder(entityType.AsEntityType().Builder);
foreach (var property in properties)
{
if (property.PropertyInfo == null)
{
continue;
}
if (property.PropertyInfo.PropertyType.IsBoolean())
{
entityTypeBuilder // (2)
.Property(property.Name)
.HasConversion(new BoolToZeroOneConverter<short>())
.HasColumnType("tinyint(1)");
}
}
}

Inject different DbContexts into generic repository based on Domain class - Autofac

In my application, I need to interact with two databases. I have two domain classes which are located in two different databases. I also have a generic repository pattern which accepts an UoW in its constructor. I am looking a way to inject appropriate UoW based on Domain class. I do not want to write second generic repository for the second database.. Is there any neat solution?
public interface IEntity
{
int Id { get; set; }
}
Located in Database A
public class Team: IEntity
{
public int Id { get; set; }
public string Name{ get; set; }
}
Located in Database B
public class Player: IEntity
{
public int Id { get; set; }
public string FullName { get; set; }
}
I also have a generic repository pattern with UoW
public interface IUnitOfWork
{
IList<IEntity> Set<T>();
void SaveChanges();
}
public class DbADbContext : IUnitOfWork
{
public IList<IEntity> Set<T>()
{
return new IEntity[] { new User() { Id = 10, FullName = "Eric Cantona" } };
}
public void SaveChanges()
{
}
}
public class DbBDataContext: IUnitOfWork
{
public IList<IEntity> Set<T>()
{
return new IEntity[] { new Tender() { Id = 1, Title = "Manchester United" } };
}
public void SaveChanges()
{
}
public interface IRepository<TEntity> where TEntity: class, IEntity
{
IList<IEntity> Table();
}
public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class, IEntity
{
protected readonly IUnitOfWork Context;
public BaseRepository(IUnitOfWork context)
{
Context = context;
}
IList<IEntity> IRepository<TEntity>.Table()
{
return Context.Set<TEntity>();
}
}
I've already found articles saying that Autofac overrides the registration with the last value. I know my problem is how DbContexts are registered.
var builder = new ContainerBuilder();
// problem is here
builder.RegisterType<DbADbContext >().As<IUnitOfWork>()
builder.RegisterType<DbBDbContext >().As<IUnitOfWork>()
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IRepository<>));
var container = builder.Build();
I inspired from #tdragon's answer.
The first step is registering Named DbContext
builder.RegisterType<Database1>()
.Keyed<IUnitOfWork>(DbName.Db1)
.Keyed<DbContext>(DbName.Db1).AsSelf().InstancePerRequest();
builder.RegisterType<Database2>()
.Keyed<IUnitOfWork>(DbName.Db2)
.Keyed<DbContext>(DbName.Db2).AsSelf().InstancePerRequest();
Please note that DbName is just an enum.
The following code scans the data access layer assembly for finding Domain classes. Then, it registers ReadOnlyRepository and BaseRepository. the place of this code is in DIConfig
Type entityType = typeof(IEntity);
var entityTypes = Assembly.GetAssembly(typeof(IEntity))
.DefinedTypes.Where(t => t.ImplementedInterfaces.Contains(entityType));
var baseRepoType = typeof(BaseRepository<>);
var readOnlyRepoType = typeof(ReadOnlyRepository<>);
var baseRepoInterfaceType = typeof(IRepository<>);
var readOnlyRepoInterfaceType = typeof(IReadOnlyRepository<>);
var dbContextResolver = typeof(DbContextResolverHelper).GetMethod("ResolveDbContext");
foreach (var domainType in entityTypes)
{
var baseRepositoryMaker = baseRepoType.MakeGenericType(domainType);
var readonlyRepositoryMarker = readOnlyRepoType.MakeGenericType(domainType);
var registerAsForBaseRepositoryTypes = baseRepoInterfaceType.MakeGenericType(domainType);
var registerAsForReadOnlyRepositoryTypes = readOnlyRepoInterfaceType.MakeGenericType(domainType);
var dbResolver = dbContextResolver.MakeGenericMethod(domainType);
// register BaseRepository
builder.Register(c => Activator.CreateInstance(baseRepositoryMaker, dbResolver.Invoke(null, new object[] { c }))
).As(registerAsForBaseRepositoryTypes).InstancePerRequest(jobTag);
//register readonly repositories
builder.Register(c => Activator.CreateInstance(readonlyRepositoryMarker, dbResolver.Invoke(null, new object[] { c })))
.As(registerAsForReadOnlyRepositoryTypes).InstancePerRequest(jobTag);
}
The following methods try to find DbSet in each DbContext in order to find out the Domain Classes belongs to which DataContext/Database.
public class DbContextResolverHelper
{
private static readonly ConcurrentDictionary<Type, DbName> TypeDictionary = new ConcurrentDictionary<Type, DbName>();
public static DbContext ResolveDbContext<TEntity>(IComponentContext c) where TEntity : class, IEntity
{
var type = typeof(DbSet<TEntity>);
var dbName = TypeDictionary.GetOrAdd(type, t =>
{
var typeOfDatabase1 = typeof(Database1);
var entityInDatabase1 = typeOfDatabase1 .GetProperties().FirstOrDefault(p => p.PropertyType == type);
return entityInDatabase1 != null ? DbName.Db1: DbName.Db2;
});
return c.ResolveKeyed<DbContext>(dbName);
}
}
What about this:
builder.RegisterType<DbContextBase>().As<IUnitOfWork>()
And
DbADataContext: DbContextBase,IUnitOfWork
DbBDataContext: DbContextBase,IUnitOfWork
Or in your registration you can just do something like :
containerBuilder.RegisterGeneric(typeof(DbADataContext<>)).Named("DbADataContext", typeof(IUnitOfWork<>));
containerBuilder.RegisterGeneric(typeof(DbBDataContext<>)).Named("DbBDataContext", typeof(IUnitOfWork<>));
If you want to keep single BaseRepository and its interface, you have to somehow configure, with entity would be handled by which DbContext. It could be done in registration part of application, but in that case you cannot register your BaseRepostory<T> as open generic, but be explicit in your registrations, like this:
containerBuilder.RegisterType<DbADataContext>().Named<IUnitOfWork>("A");
containerBuilder.RegisterType<DbBDataContext>().Named<IUnitOfWork>("B");
containerBuilder.Register(c => new BaseRepository<Team>(c.ResolveNamed<IUnitOfWork>("A")).As<IRepostory<Team>>();
containerBuilder.Register(c => new BaseRepository<Player>(c.ResolveNamed<IUnitOfWork>("B")).As<IRepository<Player>>();
(just proof of concept, code not tested)
Autofac is not smart enough to know "automatically" which unit of work you want to use in each of your repository.

Save value in uppercase when adding row to database

I have three columns in the db table that looks as follow:
When I add a new row, it should store the value on column fieldname in uppercase. How can I do that?
Since you tagged the question with entity framework, I assume you want to do it in your data layer or close to DB. There's a number of ways for doing this.
You could override SaveChanges() in your context. This will move the logic away from the model, but still ensure that the correct value is saved. Also, if you want it on several entities you can use an interface. When it's an interface you can do it for several of your entities without any duplicate code, as long as it's the same property. Otherwise you would need an attribute and reflection. Reusability is pretty high, but it adds some overhead to your SaveChanges().
public class CustomerEntity()
{
public string Name {get;set;}
}
public MyCustomContext : DbContext
{
// Other stuff...
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries<CustomerEntity>())
{
if (entry.State == EntityState.Modified || entry.State == EntityState.Added)
{
// Possibly check for null or if it's changed at all.
entry.Entity.Name = entry.Entity.Name.ToUpper();
}
}
return base.SaveChanges();
}
}
And with an interface:
public interface INameIsAlwaysUpperCase
{
string Name {get;set;}
}
public MyCustomContext : DbContext
{
// Other stuff...
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries<INameIsAlwaysUpperCase>())
{
if (entry.State == EntityState.Modified || entry.State == EntityState.Added)
{
// Possibly check for null or if it's changed at all.
entry.Entity.Name = entry.Entity.Name.ToUpper();
}
}
return base.SaveChanges();
}
}
You can add a custom validation. This will throw exception if it's not saved correctly. That way you can move the responsibility to the consumer of the model. However, depending on your scenario, you might not want to throw an exception. This is my favourite since it forces the consumer to do it the right way. As per comments, why throw when you can silently convert it? Yes, it's a valid question. For me it's about forcing the consumer of the data layer to use it correctly, and not let the daya layer decide what to to with the data. I personally don't like it when the business layer asks the data layer to save one thing, and then the data layer saves another thing. If lower case isn't a valid option, then it shouldn't be saved. I don't think it's much more different from using [Required]. But it's really about context and what works in your particular case.
public class CustomerEntity() : IValidatableObject
{
public string Name {get;set;}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Possibly check for null here as well...
if (this.Name.ToUpper() != this.Name)
{
yield return new ValidationResult("You need to save as upper!");
}
}
}
Use a property that manages this for you. This may be the simplest solution, even if I like to keep my entities "clean". It's absolutely the solution that will require least effort. However, the reusability is low, and what if you use your entitites all over the application and want the value to be lower case until it's actually saved? That's not possible. But, again, I think it comes down to your particular situation. If you want the value to be upper case even before you save it, this is probably the best solution!
public class CustomerEntity()
{
string _name;
public string Name
{
get { return _name; }
set { _name = value.ToUpper(); } // Check for null ?
}
}
Do it when saving. This moves the logic to when you're saving your entity. This is probably the least preferable option, since the reusability is non-existing. What happens in Update()? However, the OP specifically states "When I add a new row", so it may only be applicable when adding new entities. And in that case it could very well be the most prefered choice since it allows updates to have lower case. But it would have to depend on the use case.
public void AddCustomer(string name)
{
var customer = new CustomerEntity
{
Name = name.ToUpper()
};
_context.Customers.Add(customer);
}
Just use properties. If your model is as below:
public class MyModel
{
public int Id { get; set; }
public string Description { get; set; }
public string LanguageCode { get; set; }
public string FiledName { get; set; }
}
Then, change it to:
public class MyModel
{
private string fieldName;
public int Id { get; set; }
public string Description { get; set; }
public string LanguageCode { get; set; }
public string FiledName
{
get { return filedName; }
set
{
if(!string.IsNullOrEmpty(value))
fieldName = value.ToUpper();
else
fieldName = value;
}
}
}
Try this.
public string FiledName
{
get { return filedName; }
set
{
filedName = !string.IsNullOrEmpty( value ) ? value.ToUpper() : value;
}
}
Using a ValueConverter on the Property could be an effective way to do this.
public class YourDbContext : DbContext
{
public YourDbContext(DbContextOptions<YourDbContext> options)
: base(options)
{
}
public DbSet<Row> Rows { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
var converter = new ValueConverter<string, string>(
v => v.ToUpper(), // writing
v => v
);
// just one property
modelBuilder.Entity<Row>()
.Property(x => x.Column)
.HasConversion(converter);
// all of the string properties
foreach (var entityType in builder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(string))
{
builder.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(converter);
}
}
}
}
}
It's also possible to use a Custom Attribute :
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class Standardized : Attribute
{}
Then decorate properties inside your model :
public class MyModel
{
public string Id{ get; set; }
[Required]
[Standardized]
public string Description { get; set; }
}
Taken from #smoksnes accepted answer, inside your DbContext class, override SaveChanges(), SaveChangesAsync() (EF Core 5.x) and add a private method using reflection to obtain decorated properties and apply transformations, like this :
public override int SaveChanges()
{
StandardizeBeforeSaving();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
StandardizeBeforeSaving();
return await base.SaveChangesAsync(cancellationToken);
}
private void StandardizeBeforeSaving()
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.State == EntityState.Modified || entry.State == EntityState.Added)
{
var properties = entry.Entity
.GetType()
.GetProperties()
.Where(prop => Attribute.IsDefined(prop, typeof(Standardized)) && prop.PropertyType == typeof(string));
foreach (var property in properties)
{
var value = entry.CurrentValues[property.Name]?.ToString() ?? string.Empty;
entry.CurrentValues[property.Name] = value.Standardize();
}
}
}
}
Just be aware that reflection could be slower than other techniques presented in accepted answer. But for most scenarios (ie. user updates or creates couple of entities with not that many properties) it should be fine.

Categories

Resources