Repository pattern and change log implementation - c#

I have API built with .net core 2, and I am trying to implement change log feature.
I have done basic part, but I am not sure if it's a best way for doing this.
Here is my EntityBaseRepository
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IFullAuditedEntity, new()
{
private readonly ApplicationContext context;
public EntityBaseRepository(ApplicationContext context)
{
this.context = context;
}
public virtual IEnumerable<T> items => context.Set<T>().AsEnumerable().OrderByDescending(m => m.Id);
public virtual T this[int id] => context.Set<T>().FirstOrDefault(m => m.Id == id);
public virtual T GetSingle(int id) => context.Set<T>().FirstOrDefault(x => x.Id == id);
public virtual T Add(T entity) => Operations(entity: entity, state: EntityState.Added);
public virtual T Update(T entity) => Operations(entity: entity, state: EntityState.Modified);
public virtual T Delete(T entity) => Operations(entity: entity, state: EntityState.Deleted);
public virtual T Operations(T entity, EntityState state)
{
EntityEntry dbEntityEntry = context.Entry<T>(entity);
if (state == EntityState.Added)
{
entity.CreationDateTime = DateTime.UtcNow;
entity.CreationUserId = 1;
context.Set<T>().Add(entity);
dbEntityEntry.State = EntityState.Added;
}
else if (state == EntityState.Modified)
{
entity.LastModificationDateTime = DateTime.UtcNow;
entity.LastModificationUserId = 1;
var local = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(entity.Id));
if (local != null)
{
context.Entry(local).State = EntityState.Detached;
}
dbEntityEntry.State = EntityState.Modified;
}
else if (state == EntityState.Deleted)
{
entity.DeletionFlag = true;
entity.DeletionUserId = 1;
entity.DeletionDateTime = DateTime.UtcNow;
dbEntityEntry.State = EntityState.Modified;
}
return entity;
}
Here is one of my controller.
[Produces("application/json")]
[Route("api/Item")]
public class ItemController : Controller
{
private readonly IItemRepository repository;
private readonly IChangeLogRepository changeLogRepository;
private readonly IMapper mapper;
public ItemController(IItemRepository repository, IChangeLogRepository _changeLogRepository, IMapper mapper)
{
this.repository = repository;
this.changeLogRepository = _changeLogRepository;
this.mapper = mapper;
}
[HttpPost]
public IActionResult Post([FromBody]ItemDto transactionItemDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var item = repository.Add(mapper.Map<ItemDto, Item>(source: transactionItemDto));
repository.Commit();
ChangeLog log = new ChangeLog()
{
Log = "New Item Added"
};
changeLogRepository.Add(log);
changeLogRepository.Commit();
return new OkObjectResult(mapper.Map<Item, ItemDto>(source: item));
}
}
if you see in controller, I have added one item, and commited it, then I prepared log for that insertion, added and commited it.
Now, I have few questions, like
I have to commit my transaction twice, is there any way I can optimize it? I don't know if I can handle it on EntityBaseRepository or not.
I also want to check each property, if it gets changed or not. I want to log that to if it's changed. what would be the best way to handle it?
It would be great if anyone can help me with this. really appreciate. thanks.

You can use Action filters for a changelog Like
using System;
public class TrackMyChange : IActionFilter
{
private readonly string _chengeMessage;
private readonly IChangeLogRepository _changeLogRepository;
public TrackMyChange(string changeMessage,IChangeLogRepository changeLogRepository)
{
this._changeLogRepository = changeLogRepository;
this._chengeMessage = chengeMessage;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// Do something before the action executes.
}
public void OnActionExecuted(ActionExecutedContext context)
{
// Do something after the action executes.
ChangeLog log = new ChangeLog()
{Log = this._chengeMessage};
changeLogRepository.Add(log);
changeLogRepository.Commit();
}
}
In your controller, you can use it before actions you want to log like
[TrackMyChange("Your change log here")]
public IActionResult Post()
{
}
Reference :
Action Filter Attributes in .NET core

DbContext is shared between multiple repositories within the same HTTP request scope. You won't need UoW. Just try use one Commit() and see if all changes are saved in one transaction.
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implemenation-entity-framework-core
Use yourDbContext.Entry(your_entity_obj).State to get entity state. Don't log if its state is EntityState.Unchanged.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate?view=efcore-2.1

Related

EF Core In-Memory Database Not Saving To DbSet

Overview
I am currently unit/integration testing my repository pattern with the in-memory database EF Core provides. I am on version 6.0.1 for the Microsoft.EntityFrameworkCore.InMemory nuget and using Visual Studio 2022. I am running into issues saving data to the database. Below I have included snippets of my code.
My data model:
public class Example : IExample
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
My base repository class:
public class Repository<T> : IRepository<T> where T : class
private readonly DbSet<T> _dbSet;
private readonly DbContext _db;
public Repository(DbContext db)
{
_dbSet = db.Set<T>();
_db = db;
}
public virtual async Task Add(T entity)
{
await _dbSet.AddAsync(entity);
}
public virtual async Task<int> SaveAsync(CancellationToken token = default)
{
return await _db.SaveChangesAsync(token);
}
My repository class:
public class ExampleRepo : Repository<Example>
public ExampleRepo(ExampleContext db) : base(db)
{
}
My DbContext Class
I can show my IEntityConfiguration class for the ExampleBuilder shown below if it is needed but I don't believe that to be the problem.
public class ExampleContext : DbContext
private readonly IHttpContextAccessor? _httpContextAccessor;
public ExampleContext(DbContextOptions<ExampleContext> options) : base(options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
}
public ExampleContext(DbContextOptions<ExampleContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public DbSet<Example>? Examples { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
var assembly = Assembly.GetAssembly(typeof(ExampleBuilder));
if (assembly is null)
{
throw new DataException("Could not find the assembly containing the designated model builder");
}
builder.ApplyConfigurationsFromAssembly(assembly);
foreach (var entity in builder.Model.GetEntityTypes())
{
entity.SetSchema("dbo");
}
}
public override Task<int> SaveChangesAsync(CancellationToken token = default)
{
foreach (var entry in ChangeTracker.Entries()
.Where(x => x.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
{
var user = _httpContextAccessor?.HttpContext?.User?.Identity?.Name ?? "User";
entry.Property("ModifiedBy").CurrentValue = user;
if (entry.State == EntityState.Added)
{
entry.Property("CreatedBy").CurrentValue = user;
}
if (entry.State != EntityState.Deleted) continue;
entry.State = EntityState.Modified;
entry.Property("IsActive").CurrentValue = false;
}
return base.SaveChangesAsync(token);
}
My DbContext factory method:
private ExampleContext GenerateDbContext()
{
var options = new DbContextOptionsBuilder<ExampleContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ExampleContext(options);
}
My unit/integration test utilizing xUnit and NET6.0
[Fact]
public async Task GetAllEntities_ShouldReturnEntities_WhenEntitiesExist()
{
// Arrange
// I used Bogus nuget for single source for generating valid models
var entity = ExampleFaker.GetModelFaker()
.Generate();
await using var context = GenerateDbContext();
var repo = new ExampleRepo(context);
await repo.Add(entity);
var changes = await repo.SaveAsync();
// Act
// Consulted this link already. Bottom answer is most related
//https://stackoverflow.com/questions/46184937/dbcontext-not-returning-local-objects
var response = await repo.GetAll();
// Assess
TestOutputHelper.WriteLine($"Added entity to repository: {entity.ToJson()}");
TestOutputHelper.WriteLine("Expected entities saved: 1");
TestOutputHelper.WriteLine($"Actual entities saved: {changes}");
TestOutputHelper.WriteLine($"Response: {response?.ToJson()}");
// Assert
Assert.Equal(1, changes);
Assert.NotNull(response);
Assert.NotEmpty(response);
Assert.IsType<List<Example>>(response.ToList());
}
Analysis & Issue
The changes variable returns 1 so I interpret this as EF does not have any issue with my model as well as I would think it successfully saved my model in the in-memory database. However, during my GetAll retrieval, no data is returned. When I debug and look into the repository private members, it shows the DbSet is empty so it is not the GetAll method causing the issue either. Since this is also just within the scope of the unit test, I don't think my Program.cs configuration has anything to do with the issue I am seeing. I have been looking at this for quite a while and can't figure out the small detail I am probably missing for the life of me.
Thank you for your help in advance.

C# Save History Tables Interface with Entity Framework with Dependency Injection

We are using Net Core 2 with Entity Framework. Our Sql database consists of many tables, Address, Account, Customer, Sales, etc.
Some tables have corresponding history tables: AddressHistory, CustomerHistory,
Anytime someone changes something in original tables, the corresponding History table should be updated. (cannot use sql temporal tables, since we have custom history logic)
We are trying to apply interfaces, I'm little stuck on the code, can someone provide a quick code example of how to implement this with interfaces? Especially in SaveHistory portion, how can dependency injection be applied? Feel free to rewrite code as needed
public partial class TestDbContext
{
public void AddEntityHistory<IEntityHistory>(IEntityHistory entity) where IEntityHistory: Address
{
// do something 1 etc
}
public void AddEntityHistory<IEntityHistory>(IEntityHistory entity) where IEntityHistory: Customer
{
// do something 2 etc
}
public override int SaveChanges()
{
SaveEntityHistory();
return base.SaveChanges();
}
protected void SaveEntityHistory()
{
var modifiedEntities = ChangeTracker.Entries<IEntityHistory>().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach(var entity in modifiedEntities)
{
AddEntityHistory(entity); // what is the code base here? giving error below, it should call appropriate AddEntityHistory Method for corresponding Entity
}
}
}
Error Above:
Error CS0311 The type 'Interfaces.IEntityHistory' cannot be used as type parameter 'IEntityHistory' in the generic type or method 'PropertyContext.AddEntityHistory(IEntityHistory)'. There is no implicit reference conversion from 'Interfaces.IEntityHistory' to 'Data.Entities.Address'.
Resources :
Trying to utilize similar codebase: for CreateDate, UserId, etc
https://dotnetcore.gaprogman.com/2017/01/26/entity-framework-core-shadow-properties/
Generic form for methods won't work here at all. Reflection is more suitable for your requirement:
public partial class TestDbContext
{
public void AddEntityHistory(Address entity)
{
// do something 1 etc
}
public void AddEntityHistory(Customer entity)
{
// do something 2 etc
}
public override int SaveChanges()
{
SaveEntityHistory();
return base.SaveChanges();
}
protected void SaveEntityHistory()
{
var modifiedEntities = ChangeTracker.Entries<IEntityHistory>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (var entity in modifiedEntities)
{
var methodInfo = this.GetType().GetMethod(nameof(AddEntityHistory), new[] { entity.Entity.GetType() });
methodInfo.Invoke(this, new[] { entity.Entity });
}
}
}
You can use a Generic Repository and then for each Entity's repository, you can then save its respective history table. Below is the example code.
IGenericRepository
public interface IGenericRepository
{
Task GetByIdAsync(object id, CancellationToken cancellationToken = default);
Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default);
void Delete(object id);
void Delete(TEntity entityToDelete);
void Update(TEntity entityToUpdate);
void UpdateStateAlone(TEntity entityToUpdate);
}
GenericRepository
public class GenericRepository : IGenericRepository
where TEntity : class, new()
{
private readonly YourDbContext context;
internal DbSet dbSet;
public GenericRepository(YourDbContext context)
{
this.context = context;
dbSet = context.Set();
}
public virtual Task GetByIdAsync(object id, CancellationToken cancellationToken = default)
{
return dbSet.FindAsync(id);
}
public virtual Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
{
return dbSet.AddAsync(entity, cancellationToken);
}
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
public virtual void UpdateStateAlone(TEntity entityToUpdate)
{
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
Now, to use the above Generic Repository for your Entities, use the below sample code.
using System.Threading.Tasks;
public interface IAddressRepository: IGenericRepository
{
Task CommitAsync();
public virtual Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
}
using System.Threading.Tasks;
public class AddressRepository: GenericRepository, IAddressRepository
{
private readonly YourDbContext _context;
public AddressRepository(YourDbContext context) : base(context)
{
_context = context;
}
public override Task InsertAsync(Address entity, CancellationToken cancellationToken = default)
{
base.InsertAsync(entity,cancellationToken );
//call your history insert here and then the below save. This will save both the record in the main Address table and then your Address's history table.
return _context.SaveChangesAsync();
}
public Task CommitAsync()
{
return _context.SaveChangesAsync();
}
}
For detailed implementation, refer this Implement create, read, update, and delete functionalities using Entity Framework Core

Entity Framework read queries locking all database

I'm developing a web application using ASP.NET MVC and EF6 to access the database.
One of the features of my web application allow the user to download a Excel file. The query to get the information from the database takes like 5 seconds and I notice that until the query it's done we can't do anything on the rest of the web application.
Is this the normal behaviour of EF, lock the database even with AsNoTracking on the query?
If I'm not doing anything wrong and this is the default behaviour of EF how should I resolve this locking problem?
(Update)
I'm using a SQL Server database and the "lock" happens when for exemple I export the excel file and at the same time do a search that uses the same table.
To organize my code i'm using Repository and UnitOfWork pattern and to create the instances i'm using DI Unity.
The UnitOfWork implementation:
public class UnitOfWork : IUnitOfWork
{
private bool _disposed;
private DbContext _dbContext;
private Dictionary<string, dynamic> _repositories;
private DbContextTransaction _transaction;
public DbContext DbContext
{
get { return _dbContext; }
}
public UnitOfWork(DbContext dbContext)
{
_dbContext = dbContext;
}
public int SaveChanges()
{
return _dbContext.SaveChanges();
}
public IRepository<TEntity> Repository<TEntity>()
{
try
{
if (ServiceLocator.IsLocationProviderSet)
return ServiceLocator.Current.GetInstance<IRepository<TEntity>>();
if (_repositories == null)
_repositories = new Dictionary<string, dynamic>();
var type = typeof(TEntity).Name;
if (_repositories.ContainsKey(type))
return (IRepositoryAsync<TEntity>)_repositories[type];
var repositoryType = typeof(Repository<>);
_repositories.Add(type, Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), this));
return _repositories[type];
}
catch(ActivationException ex)
{
throw new ActivationException(string.Format("You need to configure the implementation of the IRepository<{0}> interface.", typeof(TEntity)), ex);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~UnitOfWork()
{
Dispose(false);
}
public void Dispose(bool disposing)
{
if(!_disposed)
{
if(disposing)
{
try
{
_dbContext.Dispose();
_dbContext = null;
}
catch(ObjectDisposedException)
{
//the object has already be disposed
}
_disposed = true;
}
}
}
}
The Repository implementation:
public class Repository<TEntity> : IRepository<TEntity>
where TEntity : class
{
private readonly IUnitOfWork _unitOfWork;
private readonly DbContext _dbContext;
private readonly DbSet<TEntity> _dbSet;
public Repository(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_dbContext = unitOfWork.DbContext;
_dbSet = _dbContext.Set<TEntity>();
}
#region IRepository<TEntity> implementation
public void Insert(TEntity entity)
{
_dbSet.Add(entity);
}
public void Update(TEntity entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
}
public void Delete(TEntity entity)
{
_dbSet.Remove(entity);
}
public IQueryable<TEntity> Queryable()
{
return _dbSet;
}
public IRepository<TEntity> GetRepository<TEntity>()
{
return _unitOfWork.Repository<TEntity>();
}
#endregion
}
The Unity configuration:
container.RegisterType<DbContext, DbSittiusContext>(new PerRequestLifetimeManager());
container.RegisterType<IUnitOfWork, UnitOfWork>(new PerRequestLifetimeManager());
//Catalog respository register types
container.RegisterType<IRepository<Product>, Repository<Product>>();
UnityServiceLocator locator = new UnityServiceLocator(container);
ServiceLocator.SetLocatorProvider(() => locator);
To create my query have to create a extension method like this:
public static Product FindPublishedAtDateById(this IRepository<Product> repository, int id, DateTime date)
{
return repository.
Queryable().
Where(p => p.Id == id).
Where(p => p.PublishedFrom <= date && (p.PublishedTo == null || p.PublishedTo >= date)).
SingleOrDefault();
}
If you're downloading a lot of data synchronously it will make the UI freeze up on you. Consider doing this asynchronously. What are you using client side, anyway?
I'm assuming you're generating an excel file from data in the database and it's just a matter of it being enough data that it takes ~5 seconds to create the file and send it to the user.

EF4.1 Code First DbContext: The operation cannot be completed because the DbContext has been disposed

I have a controller action that gets invoked directly, but throws this error:
The operation cannot be completed because the DbContext has been disposed.
I have only found solutions online regarding deferred excecution, but I don't think that applies here, because everywhere I use the context (in this instance) I call either .ToList() or .FirstOrDefault(). Here is my code:
CONTROLLER CONTENT
private IUnitOfWork UnitOfWork;
public MyFavouritesController(
IAccountServices accountServices,
IUnitOfWork unitOfWork
)
{
AccountServices = accountServices;
UnitOfWork = unitOfWork;
}
public ActionResult Index()
{
int? id = AccountServices.GetCurrentUserId();
if (!id.HasValue)
{
return RedirectToAction("Login", "Account", new { ReturnUrl = this.HttpContext.Request.Url.AbsolutePath });
}
var user = UnitOfWork.UserRepo.Get(id.Value, "Favourites", "Favourites.County", "Favourites.Country");
//THE ABOVE CALL GETS THE ERROR
//.....
return View();
}
REPOSITORY BASE CLASS
public class RepositoryBase<C, T> : IDisposable
where C:DbContext, new()
where T : ModelBase
{
private DbContext _context;
public DbContext Context
{
get
{
if (_context == null)
{
_context = new C();
this.AllowSerialization = true;
}
return _context;
}
set
{
_context = value;
}
}
public virtual T Get(int Id, params string[] includes)
{
if (Id > 0)
{
var result = Context.Set<T>().Where(t => t.Id == Id);
foreach (string includePath in includes)
{
result = result.Include(includePath);
}
return result.FirstOrDefault(); //This is where the error occurs.
}
else
{
throw new ApplicationException("Id is zero (0).");
}
}
//... (More CRUD methods)
public void Dispose()
{
if (Context != null)
{
Context.Dispose(); //Debugger never hits this before the error
}
}
}
UNIT OF WORK CLASS
public class UnitOfWork:IUnitOfWork
{
public UnitOfWork(
//... DI of all repos
IUserRepository userRepo
)
{
//... save repos to an local property
UserRepo = userRepo;
//create a new instance of the context so that all the repo's have access to the same DbContext
Context = new Context();
//assign the new context to all the repo's
//...
UserRepo.Context = Context;
}
public Context Context { get; set; }
public IUserRepository UserRepo { get; set; }
//... (some more repositories)
public void Dispose()
{
Context.Dispose(); //THIS IS NOT HIT AT ALL
}
}
LASTLY, THE MODEL CONTAINER HAS THIS LINE
_Instance.RegisterType<IUnitOfWork, UnitOfWork>(new PerThreadLifetimeManager());
As you can see, the index action will recieve a new instance of UnitOfWork which contains a new DbContext object. But at the first call to this context, it throws the above error. This pattern works everywhere else in my code.
Thanks
UPDATE
The answer below was to use a perRequestLifetimeManager. Here is the implimentation of one in unity:
public class HttpRequestLifetimeManager : LifetimeManager
{
private string _key = Guid.NewGuid().ToString();
public override object GetValue()
{
if (HttpContext.Current != null && HttpContext.Current.Items.Contains(_key))
return HttpContext.Current.Items[_key];
else
return null;
}
public override void RemoveValue()
{
if (HttpContext.Current != null)
HttpContext.Current.Items.Remove(_key);
}
public override void SetValue(object newValue)
{
if (HttpContext.Current != null)
HttpContext.Current.Items[_key] = newValue;
}
}
I noticed you're using a PerThreadLifetimeManager to control the creation and disposal of your unit of work class. You should probably change it to something like PerRequestLifetimeManager if your IoC container supports that.
Its because your are disposing the Unit Of Work, after wich you are requesting your data, store your data in a Variable after the query then you can release the Unit Of Work instance as well.

Saving data in a many-to-many relationship

I have three tables that I am working with User, Application, ApplicationAdministrator. ApplicationAdministrator is a mapping table to link User to Application which has a many-to-many relationship. I get the following error when I try to save off a new Application with a User added as an Administrator:
The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects.
So my next step was to create a BaseRepository that has a common context to pull from. However, now I get the following error when I try to modify an entity that is already attached to the context:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Why is this such a difficult process? I have seen the solutions to attach and reattach and detach and spin around on your head 5 times and then everything will work. Attaching the entities to one context ends up duplication one of the entities depending on which context I attach it to.
All help is greatly appreciated!
UserRepository.cs:
public class UserRepository : BaseRepository<User>, IUserRepository
{
// private ManagerDbContext _context = new ManagerDbContext();
public UserRepository(ManagerDbContext context)
: base(context) { }
public IQueryable<User> Users
{
get { return _context.Users.Include("Administrates").Include("Company"); }
}
public void SaveUser(User user)
{
_context.Entry(user).State = user.Id == 0 ? EntityState.Added : EntityState.Modified;
_context.SaveChanges();
}
public void DeleteUser(User user)
{
_context.Users.Remove(user);
_context.SaveChanges();
}
}
ApplicationRepository.cs:
public class ApplicationRepository : BaseRepository<Application>, IApplicationRepository
{
// private ManagerDbContext _context = new ManagerDbContext();
public ApplicationRepository(ManagerDbContext context)
: base(context) { }
public IQueryable<Application> Applications
{
get { return _context.Applications.Include("Administrators"); }
}
public void SaveApplication(Application app)
{
_context.Entry(app).State = app.Id == 0 ? EntityState.Added : EntityState.Modified;
_context.SaveChanges();
}
public void DeleteApplication(Application app)
{
_context.Applications.Remove(app);
_context.SaveChanges();
}
}
UserConfiguration.cs:
public UserConfiguration()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(x => x.FirstName).IsRequired();
this.Property(x => x.LastName).IsRequired();
this.Property(x => x.Username).IsRequired();
this.Property(x => x.CompanyId).IsRequired();
this.HasRequired(user => user.Company).WithMany().HasForeignKey(user => user.CompanyId);
this.HasRequired(user => user.Company).WithMany(company => company.Users).WillCascadeOnDelete(false);
this.HasMany(user => user.Administrates)
.WithMany(application => application.Administrators)
.Map(map => map.MapLeftKey("UserId")
.MapRightKey("ApplicationId")
.ToTable("ApplicationAdministrators"));
}
ApplicationConfiguration.cs:
public ApplicationConfiguration()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(x => x.Name).IsRequired();
this.Property(x => x.Description);
this.HasMany(application => application.Administrators)
.WithMany(user => user.Administrates)
.Map(map => map.MapLeftKey("ApplicationId")
.MapRightKey("UserId")
.ToTable("ApplicationAdministrators"));
}
Snippet for saving the entities.
long appId = Int64.Parse(form["ApplicationId"]);
long userId = Int64.Parse(form["UserId"]);
Application app = appRepository.Applications.FirstOrDefault(a => a.Id == appId);
User user = userRepository.Users.FirstOrDefault(u => u.Id == userId);
app.Administrators.Add(user);
appRepository.SaveApplication(app);
return RedirectToAction("Index");
If you are using two different contexts you must detach entity from the first one and attach it to the second one where you want to perform changes. You must also correctly configure its state. To detach entity in DbContext API you need to call:
context.Entry(entity).State = EntityState.Detached;
If you use the same context for loading all entities and saving their changes you don't need to change their state. It is done automatically by change tracking.
Btw. you should start without repository and start to use repository once you understand how does EF works and how could repository help you.
Here is my solution that I finally came up with.
I created a Func dictionary in order to attach an entity to the correct EntitySet in my context. The one downfall is that you have to hard code the EntitySet name some, so I did it in a static variable within my POCO.
BaseRepository.cs
public class BaseRepository<T> where T : class
{
public static ManagerDbContext baseContext;
public BaseRepository() { }
public BaseRepository(ManagerDbContext context)
{
baseContext = context;
}
private static object _entity;
public void AttachEntity(object entity)
{
_entity = entity;
entityAttachFunctions[entity.GetType().BaseType]();
}
private Dictionary<Type, Func<bool>> entityAttachFunctions = new Dictionary<Type, Func<bool>>()
{
{typeof(User), () => AttachUser()},
{typeof(Application), () => AttachApplication()}
};
private static bool AttachUser()
{
((IObjectContextAdapter)baseContext).ObjectContext.AttachTo(User.TableName, _entity);
return true;
}
private static bool AttachApplication()
{
((IObjectContextAdapter)baseContext).ObjectContext.AttachTo(Application.TableName, _entity);
return true;
}
}
UserRepository.cs
public void AttachEntity(object entity)
{
baseContext = _context;
base.AttachEntity(entity);
}
public void DetachUser(User user)
{
_context.Entry(user).State = EntityState.Detached;
_context.SaveChanges();
}
ApplicationRepository.cs
public void AttachEntity(object entity)
{
baseContext = _context;
base.AttachEntity(entity);
}
public void DetachApplication(Application app)
{
_context.Entry(app).State = EntityState.Detached;
_context.SaveChanges();
}
AdminController.cs
long appId = Int64.Parse(form["ApplicationId"]);
long userId = Int64.Parse(form["UserId"]);
Application app = appRepository.Applications.FirstOrDefault(a => a.Id == appId);
User user = userRepository.Users.FirstOrDefault(u => u.Id == userId);
userRepository.DetachUser(user);
appRepository.AttachEntity(user);
app.Administrators.Add(user);
appRepository.SaveApplication(app);

Categories