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.
Related
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);
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)");
}
}
}
When I tried to add objects to views, it throws exception saying unable to track an instance of type because it is a query type. Is there a way to get around this?
Query Types are read-only by definition (for all database providers, not only for in memory):
Are never tracked for changes on the DbContext and therefore are never inserted, updated or deleted on the database.
However, additionally to their usual usage scenarios of
Mapping to database views.
Mapping to tables that do not have a primary key defined.
they allow
Mapping to queries defined in the model.
or in other words
May be mapped to a defining query - A defining query is a secondary query declared in the model that acts a data source for a query type.
which is achieved with ToQuery fluent API:
Configures a query used to provide data for a query type.
So for testing query types with in memory database, you should utilize the defining query mapping capability.
For instance, inside OnModelCreating override you could add something like this:
if (Database.IsInMemory())
{
// In memory test query type mappings
modelBuilder.Query<MyQueryType>().ToQuery(() => LINQ_query);
// ... similar for other query types
}
else
{
// Database query type mappings
modelBuilder.Query<MyQueryType>().ToView("MyQueryTypeView");
// ...
}
where LINQ_query is a normal LINQ query accessing context DbSets and DbQuerys and projecting to MyQueryType.
Then the test would feed the involved entities with data and the queries using DbQuerys will retrieve the data from the defining query.
The above should be the recommended way to test views with in memory database.
Just for completeness, it's possible to directly feed the DbQuerys with data (basically mocking them) by creating some sort of query repository, but with the following restriction - it must be shared (static), because currently EF Core does not handle correctly db context members (like global query filter does) other than DbSet<T> and DbQuery<T>.
Something like this:
public static class FakeQueryProvider
{
static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
public static void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
public static IQueryable<T> GetQuery<T>()
{
lock (queries)
return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
where T : class
{
return builder.ToQuery(() => GetQuery<T>());
}
}
then instead of
.ToQuery(() => LINQ_query);
you would use
.ToFakeQuery();
and would feed it inside the test like this
List<MyQueryType> data = ...;
FakeQueryProvider.SetQuery(data.AsQueryable());
Still I recommend the first approach due to shared storage limiting the ability to run MyQueryType related tests in parallel.
I ended up refactoring Ivan's extension class code based on his suggestions/recommendations, as follows. I added overloads to the ToFakeQuery method to take in a dictionary.
public static class InMemoryQueryProviderExtensions
{
static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
public static void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
public static IQueryable<T> GetQuery<T>()
{
lock (queries)
return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
private static IQueryable<T> GetQuery<T>(Dictionary<Type, IQueryable> queryDictionary)
{
return queryDictionary.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
where T : class
{
return builder.ToQuery(() => GetQuery<T>());
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder, Dictionary<Type, IQueryable> queryDictionary)
where T : class
{
return builder.ToQuery(() => GetQuery<T>(queryDictionary));
}
}
And then, creating a new derived class for my DBContext as follows. Basically, making the derived instance of the in-memory DBContext maintain the dictionary.
public class TlInMemoryDbContext : TlDbContext
{
public TlInMemoryDbContext(DbContextOptions<TlDbContext> options)
: base(options)
{ }
Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Query<EffectiveTimeEntry>().ToFakeQuery(queries);
}
public void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
}
I used the above as suggested by Ivan Stoev.
This is how it looked
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if(Database.IsSqlServer())
{
modelBuilder.Query<ProviderRating>(entity =>
{
entity.ToView("vGetProviderRatingData", "dbo");
entity.Property(e => e.col1)
.HasMaxLength(10)
.IsUnicode(false);
entity.Property(e => e.col2)
.HasMaxLength(60)
.IsUnicode(false);
entity.Property(e => e.col3)
.HasMaxLength(10)
.IsUnicode(false);
});
}
else
{
modelBuilder.Query<ProviderRating>().ToQuery(() =>
ProviderRatingFake.Select(m => new ProviderRating()
{
col1 = m.col1,
col2 = m.col2,
col3 = m.col3,
}
));
}
}
The ProviderRatingFake class exactly similar to ProviderRating class
I also added this code in DbContext file (ProviderQualityContext)
public virtual DbSet<ProviderRatingFake> ProviderRatingFake { get; set; }
public virtual DbQuery<ProviderRating> ProviderRating { get; set; }
Then I tested like this
[TestMethod]
public void TestingWithInMemoryDb()
{
var options = new DbContextOptionsBuilder<ProviderQualityContext>()
.UseInMemoryDatabase(databaseName: "Read_From_Database")
.Options;
var fakeProviderRating = new ProviderRatingFake
{
col1 = 1,
col2 = "Something",
col3 = "Something",
};
using (var context = new ProviderQualityContext(options))
{
context.ProviderRatingFake.Add(fakeProviderRating);
context.SaveChanges();
}
//use the newly created context and inject it into controller or repository
using (var context = new ProviderQualityContext(options))
{
//use the test context here and make assertions that you are returning the
//fake data
//Note that the actual code uses the Query like this
//This query will be populated with fake data using the else block
//in the method OnModelCreating
var returnedData = this.dbContext.Query<ProviderRating>().Where(m => m.col1 ==
"Something")
}
}
public static class InMemoryQueryProviderExtensions
{
static Dictionary<Type, IQueryable> queries = new Dictionary<Type, IQueryable>();
public static void SetQuery<T>(IQueryable<T> query)
{
lock (queries)
queries[typeof(T)] = query;
}
public static IQueryable<T> GetQuery<T>()
{
lock (queries)
return queries.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
private static IQueryable<T> GetQuery<T>(Dictionary<Type, IQueryable> queryDictionary)
{
return queryDictionary.TryGetValue(typeof(T), out var query) ? (IQueryable<T>)query : Enumerable.Empty<T>().AsQueryable();
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder)
where T : class
{
return builder.ToQuery(() => GetQuery<T>());
}
public static QueryTypeBuilder<T> ToFakeQuery<T>(this QueryTypeBuilder<T> builder, Dictionary<Type, IQueryable> queryDictionary)
where T : class
{
return builder.ToQuery(() => GetQuery<T>(queryDictionary));
}
}
I'm using Abp version 3.6.2, ASP.Net Core 2.0 and free startup template for multi-page web application. I have following data model:
public class Person : Entity<Guid> {
public Person() {
Phones = new HashSet<PersonPhone>();
}
public virtual ICollection<PersonPhone> Phones { get; set; }
public string Name { get; set; }
}
public class PersonPhone : Entity<Guid> {
public PersonPhone() { }
public Guid PersonId { get; set; }
public virtual Person Person { get; set; }
public string Number { get; set; }
}
// DB Context and Fluent API
public class MyDbContext : AbpZeroDbContext<Tenant, Role, User, MyDbContext> {
public virtual DbSet<Person> Persons { get; set; }
public virtual DbSet<PersonPhone> PersonPhones { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PersonPhone>(entity => {
entity.HasOne(d => d.Person)
.WithMany(p => p.Phones)
.HasForeignKey(d => d.PersonId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_PersonPhone_Person");
});
}
}
The entities Person and PersonPhone are given here as an example, since there are many "grouped" relationships in the data model that are considered in a single context. In the example above, the relationships between the tables allow to correlate several phones with one person and the associated entities are present in the DTO.
The problem is that when creating the Person entity, I can send the phones with the DTO and they will be created with Person as expected. But when I update Person, I get an error:
Abp.AspNetCore.Mvc.ExceptionHandling.AbpExceptionFilter - The instance of
entity type 'PersonPhone' cannot be tracked because another instance
with the same key value for {'Id'} is already being tracked. When attaching
existing entities, ensure that only one entity instance with a given key
value is attached. Consider using
'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting
key values.
In addition to this, the question arises as to how to remove non-existing PersonPhones when updating the Person object? Previously, with the direct use of EntityFramework Core, I did this:
var phones = await _context.PersonPhones
.Where(p =>
p.PersonId == person.Id &&
person.Phones
.Where(i => i.Id == p.Id)
.Count() == 0)
.ToListAsync();
_context.PersonPhones.RemoveRange(phones);
_context.Person.Update(person);
await _context.SaveChangesAsync();
Question
Is it possible to implement a similar behavior with repository pattern? If "Yes", then is it possible use UoW for this?
P.S.: Application Service
public class PersonAppService : AsyncCrudAppService<Person, PersonDto, Guid, GetAllPersonsDto, CreatePersonDto, UpdatePersonDto, EntityDto<Guid>>, IPersonAppService {
private readonly IRepository<Person, Guid> _personRepository;
public PersonAppService(IRepository<Person, Guid> repository) : base(repository) {
_personRepository = repository;
}
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
var person = await _personRepository
.GetAllIncluding(
c => c.Addresses,
c => c.Emails,
c => c.Phones
)
.Where(c => c.Id == input.Id)
.FirstOrDefaultAsync();
ObjectMapper.Map(input, person);
return await Get(input);
}
}
Dynamic API calls:
// All O.K.
abp.services.app.person.create({
"phones": [
{ "number": 1234567890 },
{ "number": 9876543210 }
],
"name": "John Doe"
})
.done(function(response){
console.log(response);
});
// HTTP 500 and exception in log file
abp.services.app.person.update({
"phones": [
{
"id": "87654321-dcba-dcba-dcba-000987654321",
"number": 1234567890
}
],
"id":"12345678-abcd-abcd-abcd-123456789000",
"name": "John Doe"
})
.done(function(response){
console.log(response);
});
Update
At the moment, to add new entities and update existing ones, I added the following AutoMapper profile:
public class PersonMapProfile : Profile {
public PersonMapProfile () {
CreateMap<UpdatePersonDto, Person>();
CreateMap<UpdatePersonDto, Person>()
.ForMember(x => x.Phones, opt => opt.Ignore())
.AfterMap((dto, person) => AddOrUpdatePhones(dto, person));
}
private void AddOrUpdatePhones(UpdatePersonDto dto, Person person) {
foreach (UpdatePersonPhoneDto phoneDto in dto.Phones) {
if (phoneDto.Id == default(Guid)) {
person.Phones.Add(Mapper.Map<PersonPhone>(phoneDto));
}
else {
Mapper.Map(phoneDto, person.Phones.SingleOrDefault(p => p.Id == phoneDto.Id));
}
}
}
}
But there is a problem with removed objects, that is, with objects that are in the database, but not in the DTO. To delete them, i'm in a loop compare objects and manually delete them from the database in the application service:
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
var person = await _personRepository
.GetAllIncluding(
c => c.Phones
)
.FirstOrDefaultAsync(c => c.Id == input.Id);
ObjectMapper.Map(input, person);
foreach (var phone in person.Phones.ToList()) {
if (input.Phones.All(x => x.Id != phone.Id)) {
await _personAddressRepository.DeleteAsync(phone.Id);
}
}
await CurrentUnitOfWork.SaveChangesAsync();
return await Get(input);
}
Here there is another problem: the object, which returned from Get, contains all entities (deleted, added, updated) are simultaneously. I also tried to use synchronous variants of methods and opened a separate transaction with UnitOfWorkManager, like so:
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
using (var uow = UnitOfWorkManager.Begin()) {
var person = await _companyRepository
.GetAllIncluding(
c => c.Phones
)
.FirstOrDefaultAsync(c => c.Id == input.Id);
ObjectMapper.Map(input, person);
foreach (var phone in person.Phones.ToList()) {
if (input.Phones.All(x => x.Id != phone.Id)) {
await _personAddressRepository.DeleteAsync(phone.Id);
}
}
uow.Complete();
}
return await Get(input);
}
but this did not help. When Get is called again on the client side, the correct object is returned. I assume that the problem is either in the cache or in the transaction. What am I doing wrong?
At the moment I solved this problem.
In the beginning it is necessary to disable collections mapping, because AutoMapper rewrites them in consequence of which EntityFramework defines these collections as new entities and tries to add them to the database. To disable collections mapping, it needs to create a class that inherits from AutoMapper.Profile:
using System;
using System.Linq;
using Abp.Domain.Entities;
using AutoMapper;
namespace ProjectName.Persons.Dto {
public class PersonMapProfile : Profile {
public PersonMapProfile() {
CreateMap<UpdatePersonDto, Person>();
CreateMap<UpdatePersonDto, Person>()
.ForMember(x => x.Phones, opt => opt.Ignore())
.AfterMap((personDto, person) =>
AddUpdateOrDelete(personDto, person));
}
private void AddUpdateOrDelete(UpdatePersonDto dto, Person person) {
person.Phones
.Where(phone =>
!dto.Phones
.Any(phoneDto => phoneDto.Id == phone.Id)
)
.ToList()
.ForEach(deleted =>
person.Phones.Remove(deleted)
);
foreach (var phoneDto in dto.Phones) {
if (phoneDto.Id == default(Guid)) {
person.Phones
.Add(Mapper.Map<PersonPhone>(phoneDto));
}
else {
Mapper.Map(phoneDto,
person.Phones.
SingleOrDefault(c => c.Id == phoneDto.Id));
}
}
}
}
}
In the example above, we ignore the collection mapping and use the callback function to add, update or delete phones. Now the error about the impossibility of tracking the entity no longer arises. But if you run this code now, you can see that the returned object has both added entities and removed too. This is due to the fact that by default Abp uses UnitOfWork for application service methods. Therefore, you must disable this default behavior and use an explicit transaction.
using Abp.Application.Services.Dto;
using Abp.Application.Services;
using Abp.Domain.Repositories;
using Abp.Domain.Uow;
using Microsoft.EntityFrameworkCore;
using ProjectName.Companies.Dto;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
namespace ProjectName.Persons {
public class PersonAppService : AsyncCrudAppService<Person, PersonDto, Guid, GetAllPersonsDto, CreatePersonDto, UpdatePersonDto, EntityDto<Guid>>, IPersonAppService {
private readonly IRepository<Person, Guid> _personRepository;
private readonly IRepository<PersonPhone, Guid> _personPhoneRepository;
public PersonAppService(
IRepository<Person, Guid> repository,
IRepository<PersonPhone, Guid> personPhoneRepository) : base(repository) {
_personRepository = repository;
_personPhoneRepository = personPhoneRepository;
}
[UnitOfWork(IsDisabled = true)]
public override async Task<PersonDto> Update(UpdatePersonDto input) {
CheckUpdatePermission();
using (var uow = UnitOfWorkManager.Begin()) {
var person = await _personRepository
.GetAllIncluding(
c => c.Phones
)
.FirstOrDefaultAsync(c => c.Id == input.Id);
ObjectMapper.Map(input, person);
uow.Complete();
}
return await Get(input);
}
}
}
It is possible that such code is not optimal or violates any principles. In this case, I will be nice to know how to do it.
I know it has been a few years since this has been posted, but here is the solution we use.
public async Task<bool> UpdateDBOAsync(DboDto DBO)
{
var databaseObject = await _DBODomainService.GetDBOById(DBO.Id);
databaseObject.Name = DBO.Name;
databaseObject.Hours = DBO.Hours;
if (DBO.AddressId != null) {
databaseObject.AddressId = DBO.AddressId;
}
await _DBODomainService.UpdateDBOAsync(databaseObject);
return true;
}
Essentially, we grab the object from the database. Change the values on the object from the database. Then we save that object back to the database.
I attempting to implement the Unit of Work and Repository Pattern in my ASP.NET MVC app as described here.
I was receiving the following error:
Value cannot be null.
Parameter name: entitySet
during a query. After doing some debugging, I noticed that my DBSet<T> classes were throwing the following error:
{
"The context cannot be used while the model is being created. This exception may be
thrown if the context is used inside the OnModelCreating method or if the same context
instance is accessed by multiple threads concurrently. Note that instance members of
DbContext and related classes are not guaranteed to be thread safe."
}
System.SystemException { System.InvalidOperationException }
I have looked around Stack Overflow but cannot find a solution. I used the "Code First from Database Approach." I checked my connection string and it seems right. My version of Entity, as defined in the packages.config file, is 6.1.3. I have tried commenting out some of the relationships within the DbContext class. The definition of which is as follows:
public partial class BKTrainerContext : DbContext
{
public BKTrainerContext()
: base("name=BKTrainerContext")
{
}
public virtual DbSet<AccessLevel> AccessLevels { get; set; }
public virtual DbSet<BuildCardCategory> BuildCardCategories { get; set; }
public virtual DbSet<BuildCard> BuildCards { get; set; }
public virtual DbSet<Manager> Managers { get; set; }
public virtual DbSet<SliderImage> SliderImages { get; set; }
public virtual DbSet<StoreMessage> StoreMessages { get; set; }
public virtual DbSet<Store> Stores { get; set; }
public virtual DbSet<Video> Videos { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<AccessLevel>()
.HasMany(e => e.Managers)
.WithRequired(e => e.AccessLevel1)
.HasForeignKey(e => e.AccessLevel)
.WillCascadeOnDelete(false);
modelBuilder.Entity<BuildCardCategory>()
.HasMany(e => e.BuildCards)
.WithRequired(e => e.BuildCardCategory1)
.HasForeignKey(e => e.BuildCardCategory)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Manager>()
.HasMany(e => e.StoreMessages)
.WithRequired(e => e.Manager)
.HasForeignKey(e => e.MessageAuthor)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Manager>()
.HasMany(e => e.Stores)
.WithRequired(e => e.Manager)
.WillCascadeOnDelete(false);
modelBuilder.Entity<StoreMessage>()
.Property(e => e.MessageBody)
.IsUnicode(false);
modelBuilder.Entity<StoreMessage>()
.HasMany(e => e.Stores)
.WithMany(e => e.StoreMessages)
.Map(m => m.ToTable("MessagesStores").MapLeftKey("MessageID").MapRightKey("StoreID"));
}
}
This is my GenericRepo class
public class GenericRepo<T> : IGenericRepo<T> where T : class
{
internal BKTrainerContext dbContext;
internal DbSet<T> db;
public GenericRepo(BKTrainerContext context)
{
this.dbContext = context;
this.db = context.Set<T>();
}
public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
{
IQueryable<T> query = db;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual T GetEntity(object id)
{
return db.Find(id);
}
public virtual void Insert(T newEntity)
{
db.Add(newEntity);
}
public virtual void Delete(object id)
{
T enity = db.Find(id);
Delete(enity);
}
public virtual void Delete(T entity)
{
if (dbContext.Entry(entity).State == EntityState.Detached)
{
db.Attach(entity);
}
db.Remove(entity);
}
public virtual void Update(T entity)
{
db.Attach(entity);
dbContext.Entry(entity).State = EntityState.Modified;
}
}
The original error is thrown by the lines within Get(); those that call a query.xx method.
My Unit of Work class is as follows:
public class UnitOfWork : IUnitOfWork
{
private BKTrainerContext dbContext = new BKTrainerContext();
private GenericRepo<BuildCard> buildCardRepo;
private GenericRepo<BuildCardCategory> buildCardCategoryRepo;
private GenericRepo<Manager> managerRepo;
private GenericRepo<StoreMessage> messageRepo;
private GenericRepo<SliderImage> sliderRepo;
private GenericRepo<Store> storeRepo;
private GenericRepo<Video> videoRepo;
private bool disposed = false;
public GenericRepo<BuildCard> BuildCardRepo
{
get
{
if (this.buildCardRepo == null) { buildCardRepo = new GenericRepo<BuildCard>(dbContext); }
return buildCardRepo;
}
}
public GenericRepo<BuildCardCategory> BuildCardCategoriesRepo
{
get
{
if (this.buildCardCategoryRepo == null) { buildCardCategoryRepo = new GenericRepo<BuildCardCategory>(dbContext); }
return buildCardCategoryRepo;
}
}
public GenericRepo<Manager> ManagerRepo
{
get
{
if (this.managerRepo == null) { managerRepo = new GenericRepo<Manager>(dbContext); }
return managerRepo;
}
}
public GenericRepo<StoreMessage> MessageRepo
{
get
{
if (this.messageRepo == null) { messageRepo = new GenericRepo<StoreMessage>(dbContext); }
return messageRepo;
}
}
public GenericRepo<SliderImage> SliderRepo
{
get
{
if (this.sliderRepo == null) { sliderRepo= new GenericRepo<SliderImage>(dbContext); }
return sliderRepo;
}
}
public GenericRepo<Store> StoreRepo
{
get
{
if (this.storeRepo == null) { storeRepo= new GenericRepo<Store>(dbContext); }
return storeRepo;
}
}
public GenericRepo<Video> VideoRepo
{
get
{
if (this.videoRepo == null) {videoRepo = new GenericRepo<Video>(dbContext); }
return videoRepo;
}
}
public void Save()
{
dbContext.SaveChanges();
}
protected virtual void Dispose(bool disposing) {
if (!this.disposed)
{
if (disposing)
{
dbContext.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
I am using Unity to inject it into my Controller.
public class HomeController : Controller
{
private IUnitOfWork worker;
public HomeController(IUnitOfWork unit)
{
this.worker = unit;
}
public ActionResult Index(bool failedLogin = false)
{
IndexViewModel vm = new IndexViewModel();
vm.invalidLogin = failedLogin;
return View(vm);
}
[HttpPost]
public ActionResult AttemptLogin(string userName, string password)
{
List<Store> stores = worker.StoreRepo.Get().ToList();
Store store = worker.StoreRepo.Get(u => u.Username == userName && u.Password == password).FirstOrDefault();
if (stores != null)
{
Session["UserLevel"] = 0;
Session["UserID"] = store.ID;
return RedirectToAction("Index", "User");
}
else
{
Manager manager = worker.ManagerRepo.Get(m => m.Username == userName && m.Password == password).FirstOrDefault();
if (manager != null)
{
Session["UserLevel"] = manager.AccessLevel;
Session["UserID"] = manager.ID;
return RedirectToAction("Index", "Admin");
}
else
{
return RedirectToAction("Index", new { failedLogin = true });
}
}
}
}
I would really appreciate any help. Thanks!
EDIT:
Injection configuration:
public class UnityConfig
{
#region Unity Container
private static Lazy<IUnityContainer> container = new Lazy<IUnityContainer>(() =>
{
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
/// <summary>
/// Gets the configured Unity container.
/// </summary>
public static IUnityContainer GetConfiguredContainer()
{
return container.Value;
}
#endregion
/// <summary>Registers the type mappings with the Unity container.</summary>
/// <param name="container">The unity container to configure.</param>
/// <remarks>There is no need to register concrete types such as controllers or API controllers (unless you want to
/// change the defaults), as Unity allows resolving a concrete type even if it was not previously registered.</remarks>
public static void RegisterTypes(IUnityContainer container)
{
// NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
// container.LoadConfiguration();
container.RegisterType<IUnitOfWork, UnitOfWork>();
// container.RegisterType<IProductRepository, ProductRepository>();
}
}
}
The UnityMVCActivator class
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(BKBuildCard.App_Start.UnityWebActivator), "Start")]
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(BKBuildCard.App_Start.UnityWebActivator), "Shutdown")]
namespace BKBuildCard.App_Start
{
/// <summary>Provides the bootstrapping for integrating Unity with ASP.NET MVC.</summary>
public static class UnityWebActivator
{
/// <summary>Integrates Unity when the application starts.</summary>
public static void Start()
{
var container = UnityConfig.GetConfiguredContainer();
FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(container));
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
/// <summary>Disposes the Unity container when the application is shut down.</summary>
public static void Shutdown()
{
var container = UnityConfig.GetConfiguredContainer();
container.Dispose();
}
}
}
EDIT
I attempted to run the application by using a concrete implementation of the class, but the problem persists.
private UnitOfWork worker;
public HomeController() {
this.worker = new UnitOfWork();
}
Just change one line in your RegisterTypes method:
container.RegisterType<IUnitOfWork, UnitOfWork>(new TransientLifetimeManager());
or try changing to
container.RegisterType<IUnitOfWork, UnitOfWork>(new PerThreadLifetimeManager());
What is happening here is that multiple threads are trying to share the same DbContext. The second one tries querying the model while the model is still being built on first thread. The solution would be to make sure that no 2 threads share the same DbContext. If the DbContext would be different on different threads, each one would build and query model on its own.
Finally figured it out. I neglected to mark a customer property on my class as [NotMapped].
public HttpPostedFileBase cardImg { get; set; }
The correction simply involved adding the attribute.
[NotMapped]
public HttpPostedFileBase cardImg { get; set; }
I was under the impression that the following error:
Value cannot be null.
Parameter name: entitySet
was caused by the context issues. Evidently, it was the reverse; the null parameter issue was causing "Context cannot be used" error. This is a good summary of resolving issues related to null entitySet parameters (which can cause the context issues noted above).
The solution was simple, but, it would not have been my initial guess based upon the error message. As such, I hope somebody may be able to gain some use insight from my foolish mistake.