MVC Mapping layer accessing Service layer - c#

I have designed my application (ASP.NET MVC with EF6) so there is a Mapping layer responsible of holding mappings from Entities to ViewModels back and forth. It is used by the Controllers, so basically the usual Edit action looks like
var entity = DataService.Get(viewModel.Id);
entity = Mapper.MapToEntity(viewModel, entity);
DataService.Update(entity);
The base MapToEntity implementation uses AutoMapper to map the basic properties of the object.
Update() is implemented in EF6 as a function which marks the entity as modified and calls the underlying context SaveChanges().
However, for more complex mappings, like entities with related entities, e.g. an Order, the Mapper class performs more operations. For example, updating the related entity objects (OrderDetails) with the ViewModel related object data, assigning the parent Id (OrderId) to the new child objects, and finally the problematic issue: deleting a child object.
Right now, my specialised Mappers have a reference to the Service Object of the related entity, which has a MarkEntityToDelete function, called just to mark in the underlying DbSet that entity for deleting.
foreach (var orderDetail in viewModel.OrderDetails.Where(d => d.Id != 0 || d.Delete == false))
{
var orderDetailToUpdate = orderDetail.Id == 0 ? new OrderDetail() : _orderDataService.GetOrderDetail(orderDetail.Id);
if (orderDetail.Delete)
{
_orderDataService.MarkLineaAsDeleted(orderDetailToUpdate);
}
else
{
Mapper.Map(orderDetail, orderDetailToUpdate);
if (orderDetail.Id != 0) continue;
orderDetailToUpdate.MantenimientoId = order.Id;
order.OrderDetails.Add(orderDetailToUpdate);
}
}
This is a dependency which is only there because the implementation uses EF6 and the transaction border is inside the Service, so the Mapper cannot directly delete the object, just mark it for deleting.
Although this solution works well in practice, I don't particularly like it very much. I don't think the Mapper object should have a dependency on a Service object in this way, just to mark a child object for deleting.
I tried to use the following code:
entity.EntregasCuenta.Remove(entity.EntregasCuenta.Single(e => e.Id == entregaCuenta.Id));
but it gives a foreign key check error, which is expected, as I don't delete the related entity but the relationship.
So my question is, how should the mapper deal with this situation? Mapping from a ViewModel to the Entity should be done by a Mapping layer, but updating the related entity should be done by a Service. Finally, the Controller responsibility should be only to call the three operations in order, that is, retrieve entity (service), map viewmodel data to entity (mapping layer), update changes (service).
Am I missing something?

Related

.NET 6 Entity Framework Tracking with Dependency Injection

I guess I just don't understanding EF tracking. I have the context added via dependency injection via:
builder.Services.AddDbContext<OracleContext>(options => options.UseOracle(OracleConnectionString, b => b.UseOracleSQLCompatibility("11"))
.LogTo(s => System.Diagnostics.Debug.WriteLine(s))
.EnableDetailedErrors(Settings.Dev_System)
.EnableSensitiveDataLogging(Settings.Dev_System)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
I set the tracking behavior to NoTracking here (at least so I thought).
I have a .NET Controller that has the context in its constructor. It passes this context to my class constructor containing my methods. Pretty much everything works fine... except for one:
I have a method that does a context.RawSqlQuery to get a list of objects. I iterate over these objects calling two separate methods from a different class that was generated the same way (using the injected context). This method first does a EF query to verify the object does not already exist, if it does it returns it and we move on - no issues. On the query to check if it exists I also added .AsNoTracking() for SnGs. However, if the object does not exist, and I try to make a new one... every time I do an context.add I get
"The instance of entity type 'Whatever' cannot be tracked because another instance with the key value '{MfrId: 90}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached."
I have tried adding
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking - no change
I have tried adding context.Entry(NewItem).State = EntityState.Detached; before and after the call - no change.
I tried a loop in the context that gets all tracked objects and sets them to detached - no change.
What am I missing here? First - why is it tracking at all? Second - any suggestions on how to get passed this? Should I just give up using dependency injection for the context (suck... lots of rework for this)?
As requested - here is the class & method that is failing (non related stuff removed):
public class AssetMethods : IAssetMethods
{
public OracleContext db;
public AssetMethods(OracleContext context)
{
db = context;
}
public CcpManufacturer? CreateNewManufacturer(CcpManufacturer NewMan, string ActorID)
{
...blah blah non DB validation stuff removed...
//Check if it exists already
CcpManufacturer? OldMan = db.CcpManufacturers.Where(m=>m.MfrName == NewMan.MfrName).AsNoTracking().FirstOrDefault();
if (OldMan != null) {
return OldMan;
}
//Who done did it
NewMan.CreatedBy = ActorID;
NewMan.CreationDate = DateTime.Now;
NewMan.Isenabled = true;
//save
db.CcpManufacturers.Add(NewMan);
db.SaveChanges(); //fails here
//Prevent XSS Reflection
return db.CcpManufacturers.Find(NewMan.MfrId);
}
}
this method is called from this code. The OM is also using the injected context
List<MasterItem> Items = OM.RawSqlQuery(Query, x => new MasterItem { MFR_NAME = (string)x[0], MODEL_NUMBER = (string)x[1], LEGAL_NAME= (string)x[2]});
foreach (MasterItem item in Items)
{
CcpManufacturer? Man = new() {
MfrName = item.MFR_NAME,
Displayname = item.MFR_NAME
};
Man = AM.CreateNewManufacturer(Man,System.Id); //if its new.. it never get passed here because of the discussed error...
if (Man == null || Man.MfrId == 0)
{
continue;
}
.... other stuff
}
So the mfr id is added to a new object that's passed to a pretty much identical methods to create a item (where the mfr id is attached). Now - if I detach THAT item - I am ok. But why is it tracking when I have it turned off pretty much everywhere?
Yes, you found your problem.
Turning off Tracking affects what EF does when querying for entities. This means when I tell EF to read data from the DB and give me entities, it will not hang onto references of those entities.
However, entities you tell a DBContext to ADD to a DbSet and related entities will be tracked, regardless of your tracking setting.
So if I do something like:
var entity1 = context.Entities.Single(x => x.Id == entityId).AsNoTracking();
var entity2 = context.Entities.Single(x => x.Id == entityId).AsNoTracking();
The references to entity1 and entity2 will be 2 distinct references to the same record. Both are detached, so the DbContext isn't tracking either of them. I can use and attach either of them to perform an update, but that entity would be from that point considered Attached until I explicitly detach it again. Attempting to use the other reference for an update would result in that error. If I query specifying NoTracking after I have attached and updated that first entity reference, I will get back a new untracked entity reference. The DbContext doesn't return it's tracked reference, but it doesn't discard it either.
The exact same thing happens if I add a new entity then query for it specifying NoTracking. The query returns an untracked reference. So if you try and attach it to update a row, EF will complain about the reference it is already tracking.
I don't recommend diving down the rabbit hole of passing around detached entities unless you're keen to spend the time to really understand what is going on behind the scenes and prepared for the pretty deliberate and careful handling of references. The implications aren't just things not working as expected, it's having things work or not work on a completely situational basis which can be a nasty thing to debug and fix, even when you know what to look for.

How can I attach an entity to the context when one of its children is already attached?

Background
In my application we were running into issues when trying to add a new entity with existing children to the database after mapping it from a DTO using AutoMapper. The entity and its children were not attached to the context, and so the children would be treated as new and EF would attempt to insert duplicates. My solution to this was to automatically attach an entity to the context whenever an object was mapped to a BaseEntity type (BaseEntity is the base class for all of our Model objects, it has an Id property and nothing else). Here is the code:
public TDestination Map<TDestination>(object source) where TDestination : class
{
var result = _mapper.Map<TDestination>(source);
if (typeof(TDestination).IsSubclassOf(typeof(BaseEntity)) && result != null)
_context.Attach(result); //_context is a DbContext
return result;
}
This worked fine in my initial test cases, but now I've run into an issue where the entity I'm attaching has a child that is already attached to the context. This throws "The instance of entity type 'MyChildEntity' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.".
How can I attach an entity to the context when a child is already attached? I'm trying to keep this method extremely generic so that it can be used by any object that we are trying to map from a DTO to a BaseEntity.
What I've Tried
I've tried grabbing the associated EntityEntry and recursively detach all of its children using the following method before attempting to call Attach():
private void DetachChildren(EntityEntry entity)
{
foreach (var member in entity.Members.Where(x => x.CurrentValue != null))
{
if (IsBaseEntityType(member.CurrentValue.GetType()))
{
var childEntity = _context.Entry(member.CurrentValue);
childEntity.State = EntityState.Detached;
DetachChildren(childEntity);
}
}
}
I've confirmed that this loop does reach the problem child and sets its state to detached, but I still end up getting the same error when calling Attach().
Welcome to the hell that is working with detached entities.
Automapper can be leveraged to update existing entities rather than forming an entity and attaching it. Given an object like an Order:
public void UpdateOrder(OrderDTO orderDTO)
{
var order = _context.Orders.Single(x => x.OrderId = orderDTO.OrderId);
_mapper.Map(orderDTO, order);
_context.SaveChanges();
}
The benefits of this approach is that it handles whether the order happens to be tracked or not, asserts the order exists for something like an Update where it is assumed it does, and when SaveChanges runs, only the fields that actually changed will be updated. If only 1 field changed, the update statement updates that single field. Attaching a new object and setting EntityState to Modified will update all fields. This could introduce unexpected attack vectors to change data you don't expect since a DTO needs to pass enough info to construct a whole entity to avoid unintentionally #null-ing data. The mapping from DTO to entity should ensure that only editable fields are copied across.
In the case where the OrderDTO will contain one or more child collections to update, you will likely need to use a mapping that excludes those collections, then use AfterMap in the mapping configuration to inspect the child collection for new vs. existing vs. removed entities and handle those accordingly. (Add vs. mapper.Map vs. Remove)
Generally the updates can be structured to perform atomic operations that make the entity interactions as straight forward as possible. For instance UpdateOrderDetails(orderDTO) would update information about the order itself, where there would be separate methods such as AddOrderLine(newOrderLineDTO) vs. UpdateOrderLine(orderLineDTO) vs. RemoveOrderLine(orderLineId) etc. rather than having all order line operations and other related changes done through a single UpdateOrder method accepting a whole modified object graph.
The alternative when dealing with graphs and the possibility of tracked entities is that you need to check each and every related entity against the DbSet's .Local or other means to check to see if the entity is tracked. If it is, then you have to replace the references and copy any applicable changes to the already tracked entity. Telling a DbContext to ignore an existing entity isn't always a simple matter as there can be references to that entity in other tracked entities. Generally you'll want to detect a tracked entity reference then update your references to use that tracked reference and update it as needed. It is lots of mucking around with references, and definitely does not work well with Generic methods
Generic operations are tempting from a DNRY standpoint, but dealing with situations where some entities might be tracked vs. not, and then handling type mismatches etc. (source = object = can be anything..) adds a lot of complexity in place of simpler methods to handle operations.

Is it really necessary to ignore self-referencing loops while using many-to-many relationships in ef core?

I'm trying to set up a many-to-many relationship on ef core following this model:
https://stackoverflow.com/a/46184785/11234800
But every time I try to query a person with all its clubs from this relationship, as follows:
public async Task<IList<Person>> GetAll()
{
var query = _dbContext.Set<Person>()
.Include(pc => pc.PersonClubs).ThenInclude(c => c.Club)
.AsQueryable();
return await query.ToListAsync();
}
I end up running in a self-referencing loop error, which I solved by doing this: https://stackoverflow.com/a/34847316/11234800
Is it really necessary to ignore this type of error or there's a better way to solving this issue?
IMO the self-referencing loop issue is only a result from a poor design choice in your application.
I would recommend you not to return the entity which is connected to EF and instead return a model which represents the application endpoint interface, a data transfer object.
That object will only present the necessary data, which should be presented from that specific endpoint.
For example, if you return the Person entity as you do, is the relation property PersonClubsId really needed to represent a person?
Or for instance, if you have some metadata field like CreatedDate, CreatedBy. Those should most likely not be included.
Instead create your own class which will represent a Person with the properties that represents your entity in the best way.
Another reason why you should decouple the EF entity from your application endpoint interface is because if you make any changes in the db-structure for your Person entity,
those changes will be reflected upon the client as they both uses the same model.

WebApi receiving model with references (Not attached to Context)

I am trying to pass a complex type to WebApi having this on my ApiController :
[HttpPost]
public void DoSomeCrud(JObject data)
{
ComplexModel item = data.ToObject<ComplexModel>();
// Do some logic here
}
My issue is that one of the properties I have inside my ComplexModel is an Entity Framework entity. I don't have problems passing that entity if detached, however as soon as I get that entity from DbContext the model cannot be passed to WebApi as expected.
My question is.. : Is there anyway to detach my entity preserving my references to foreign keys ? Because I need those references on the WebApi side.
Thanks
It is not best practice to use model from entity framework as data transfer object (Dto) for Web Api because you can get problem with serialization since models from EF are actually proxies which supports lazy loading and navigation properties (if you don't detach it).
The best practice is, for separation of concern, you should define your own Dto objects instead of using entity models directly from EF.
Simple example, if you have Customer entity, you also should have CustomerDto entity which projects any property from Customer you want.

Entity Framework Partial Classes Instantiate by Id / Primary Key

I'm using Entity Framework 4.1. I have a normal model .edmx which maps a Match class to a 'Match' database table and this can be accessed as normal using EF.
However I require custom properties methods for the Match so I extended this using a partial class and I can add my properties etc.
All of this works fine, however I just can't find out how to instantiate an instance of my partial match class by its primary key / id. i.e so I can pass the Id into the constructor and have the object populated with all of its data from the database.
I know we can do the following to populate from calling code:
public Match PopulateforMatchId(int matchId)
{
var match = (from m in _db.Matches
.Include("TeamA")
.Include("TeamB")
.Include("Season")
.Include("Season.Competition")
where m.Match_ID == matchId
select m).FirstOrDefault();
return match;
}
However this is not what I need as this is not self contained within the partial class itself, I need it to populate itself, as other properties in the partial class rely on the object itself having its data in place before they can be calculated.
Anyone have any ideas how i can do this?
Thanks
Kevin
This is wrong way to use Entity framework. Entity framework is not suitable for easy populating existing object. Moreover it demands that entity has internal dependency on the EF context.
How to probably make it work (but I definitely not recommend it):
public Match(int matchId)
{
// You called constructor yourselves = you have unproxied entity without
// dynamic change tracking and without lazy loading
Id = matchId;
// I'm not sure if this is possible in entity constructor but generally it should work
// Get context somewhere - service locator pattern
ObjectContext context = ContextProvider.GetCurrent();
context.Matches.Attach(this);
context.Refresh(RefreshMode.StoreWins, this);
// Now you should have populated Match entity itself but not its navigation properties
// To populate relations you must call separate query for each of them because `Include`
// is possible only when the entity is created and loaded by EF and lazy loading is off
// in this case
context.LoadProperty(this, m => m.TeamA);
context.LoadProperty(this, m => m.TeamB);
Season = (from s in context.Seasons.Include("Competition")
select s).ToList();
}
This is also the example of wrong constructor - constructor should not take such heavy logic. It should be responsibility of some other initialization method.

Categories

Resources