How to avoid circular loop in Linq to Sql query? - c#

Currently I am using [JsonIgnore] property to avoid circular references in the models. But I think, it will be applied in all implementations.
Right now I am using like this-
I have two Models:
public class Project
{
public int ProjectID { get; set; }
[Required]
public string ProjectName { get; set; }
public DateTime? EstimatedEndDate { get; set; }
public DateTime? ProjectStartDate { get; set; }
public string OrderNumber { get; set; }
public string ProjectDescription { get; set; }
public virtual List<ServiceObject> ServiceObjects { get; set; }
[Required]
public string RegardingUser { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
public bool IsProjectDeleted { get; set; }
[NotMapped]
public int? ServiceObjectTemplateID { get; set; }
}
One another Model is-
public class ServiceObject
{
public int ServiceObjectID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string RegardingUser { get; set; }
public int? ParentServiceObjectID { get; set; }
[ForeignKey("ParentServiceObjectID")]
public virtual List<ServiceObject> ServiceObjects { get; set; }
public int ProjectID { get; set; }
[JsonIgnore]
public virtual Project Project { get; set; }<---------------------------
public virtual List<ServicePicture> ServicePictures { get; set; }
public bool IsServiceObjectDeleted { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string ModifiedBy { get; set; }
public DateTime ModifiedDate { get; set; }
}
But I want to apply JsonIgnore property or any other property In Linq to Sql query itself. I mean I want to apply it dynamically.
The Query is like-
var projectList = (from pro in context.Projects.All
select pro).ToList();
I want to apply conditionally.
Thanks in advance.

XY problem. Your actual problem is "I want to control at runtime what properties get serialized".
The most common method of controlling what properties are serialized is using the JsonIgnoreAttribute. However as others have commented, attributes are applied at compile time, and cannot normally be modified at runtime (nor would you want to, since it would in effect be a global setting being changed at runtime, you would run into a ton of Threading issues).
Instead, the answer is to use the IContractResolver interface to change the serialization behavior.
public class OmitPropertyContractResolver
: IContractResolver
{
private readonly string[] _ignoredProperties;
private readonly IContractResolver _resolver;
public OmitPropertyContractResolver(IContractResolver resolver, params string[] ignoredProperties)
{
_ignoredProperties = ignoredProperties;
_resolver = resolver;
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = _resolver.CreateProperties(type, memberSerialization);
return properties
.Where(p => _ignoredProperties.Contains(p.Name) == false)
.ToList();
}
}
The other possibility is to use ReferenceLoopHandling setting in your Json.net settings.
var serializer = new JsonSerializer(
new JsonSerializerSetting()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
} );
This will serialize each object only once.
The last option is to use ItemReferenceLoopHandling. This is a non-standard way of serializing (and deserializing) Object-Graphs (note, graph and not tree) which have circular references.
The disadvantage of this method is that it is non-standard, and might not work on the client end.

Ok, so based on your inputs and as I suspected, what you're seeing is a typical EF Code First issue. EF generates dynamic proxies for each of your entity so that it can track changes to it. However, this makes serialization difficult since they are created as dynamic objects. There are few ways to avoid this:
Option A: Disable proxy creation just for the particular query:
// disable proxy creation just for the duration of query
context.Configuration.ProxyCreationEnabled = false;
// do the query, serialize, go crazy!
// enable proxy again. or if your context goes out of scope after this call, then you can ignore re-enabling.
context.Configuration.ProxyCreationEnabled = true;
Option B: If you are not going to modify and update the entity at some later point
var entity = context.Where(...); // or whatever query
// detach entity
context.Entry(entity).State = EntityState.Detached;
Option C: Avoid proxies by modifying type definition:
If you create sealed type or type without virtual properties, EF will not create a proxy since there
is nothing for the proxy to do.
public sealed class Project {...}
// or make it non virtual
...
public List<ServiceObject> ServiceObjects { get; set; }
...
Note that in this case, you have to load related objects manually:
context.Projects.Include(p => p.ServiceObjects).ToList();
Option D: Disable proxy creation permanently:
// in your initializer or DbContext class' constructor
DbContext.Configuration.ProxyCreationEnabled = false;
Note: If proxy creation is disabled, EF will not tracked changes automatically. You can manually track them using the Snapshot method.
You can follow one or more of these options depending on your design. I usually stick with option A or B.

Related

Mapster - Dynamic property selection or ignore

I have a business need to dynamically select ONLY the properties of a given model that are specified, similar to an OData select clause. I am currently using Mapster's ProjectToType functionality to populate view models from EF Core entities.
Is there any way to tell Mapster to only select a given list of properties in the query that it generates? Or a way to take the full model mapping, and change mappings at runtime in an instance of TypeAdapterConfig to ignore properties that aren't in a given list of properties?
The end solution needs to be generic and work with navigation properties, because it will be applied to all of our entities in the database. We also used DynamicLinq in some cases, not sure if that can be used on top of Mapsters ProjectToType functionality.
Example:
Entities (Some properties omitted for length):
namespace DataAccess.Entities
{
public class Series
{
public Guid Id { get; set; }
public string Description { get; set; }
public long? StackRank { get; set; }
public string EntityId { get; set; }
// Other properties
}
public class Model
{
public Guid Id { get; set; }
public string Description { get; set; }
public string EntityId { get; set; }
public long? StackRank { get; set; }
public Guid SeriesId { get; set; }
public virtual Series Series { get; set; }
// Other properties
}
}
View Models (Some properties omitted for length):
namespace Models
{
public class Model
{
public Guid Id { get; set; }
public string Description { get; set; }
public string EntityId { get; set; }
public long? StackRank { get; set; }
public Guid SeriesId { get; set; }
public virtual Series Series { get; set; }
// Other properties
}
public class Series
{
public Guid Id { get; set; }
public string Description { get; set; }
public long? StackRank { get; set; }
public string EntityId { get; set; }
// Other properties
}
}
Given a rest call to get a list of all Model view models, with this list of properties to include:
var properties = new List<string> {
"Id",
"EntityId"
"Description",
"Series.Id",
"Series.Description",
"Series.EntityId"
}
The results would return some type of dictionary, dynamic, or anonymous object that contained ONLY these properties, and the other properties would not even be included in the final select of the SQL query that gets created.
In the end, I decided to use Arca Artem's suggestion, with a little twist. I used reflection to grab a list of all properties of the model and cache them. After that, I compared the cached properties vs the list of properties to include, and ignored the properties that weren't in both lists. Kinda like this:
var clonedConfig = mapsterInstance.Clone();
clonedConfig.ForType<TSource, TDestination>().Ignore(propertiesToIgnore);
var models = await query.ProjectToType<TDestination>(clonedConfig).ToListAsync();
Maybe not the most elegant solution, but it worked well enough for what I needed. I also set up our json serializer to ignore null values.

Entity framework core load navigation properties for recursive table

I am using Entity Framework Core 5.0 and the following model:
So a Job has a MainFlow, and the mainflow can have 0 or more Subflows which in turn can have subflows (recursive)
The way this has been setup is that I have a Job entity which has a MainflowId property (and also navigation property)
The Flows have a ParentFlowId property, and a navigation property with a collection of SubFlows.
I also included a TreeId property on Job and Flow to easily identify a tree.
public class Job
{
public int Id { get; set; }
public DateTimeOffset Timestamp {get; set; }
public string JobName{ get; set; }
public int MainFlowId { get; set; }
public Guid TreeId { get; set; }
public virtual Flow MainFlow { get; set; }
}
public class Flow
{
public int Id { get;set; }
public string Name { get; set; }
public string DisplayName { get; set; }
public Guid TreeId { get; set; }
public int? ParentFlowId { get; set; }
public virtual Flow ParentFlow { get; set; }
public virtual ICollection<Flow> SubFlows { get; set; } = new List<Flow>();
}
What I am trying to achieve is to load a list of jobs (for example based upon the timestamp), but in a way that all the navigation properties become available (Job->Mainflow->Subflow->Subflow->...)
I was able to get this behavior by loading the jobs, and then loading the flows separately using the TreeId, however because of performance issues I am now using .AsNoTracking() and this does not seem to work anymore.
Is there a way to load the whole trees with their navigation properties while using .AsNoTracking?
Thanks for any insight!
Edit:
Here is the way it is working without the AsNoTracking()
(I simplified the code a bit)
private IQueryable<Job> GetAllJobs()
{
return DbContext.Set<Job>()
.Include(a=>a.MainFlow)
}
private IEnumerable<Flow> GetAllFlowsForTreeIds(IEnumerable<Guid> treeIds)
{
var result = from flow in DbContext.Set<Flow>()
.Include(a => a.ParentFlow)
.AsEnumerable()
join treeId in treeIds
on flow.TreeId equals treeId
select flow;
return result;
}
public IEnumerable GetJobTrees()
{
var allJobs = GetAllJobs().ToList();
var flows = GetAllFlowsForTreeIds(allJobs.Select(a=>a.TreeId)).ToList());
//by getting the flows, the navigation properties in alljobs become available
}

Entity Framework .Include throwing NullReferenceException

I have a very basic EF setup that is throwing an odd error when trying to populate a navigation property by using .Include. Here are the entity Models:
public class LineGroup
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public ICollection<LineGroupMember> LineGroupMembers { get; set; }
}
public class LineGroupMember
{
public int ID { get; set; }
public int Extension { get; set; }
public string Name { get; set; }
public int Permissions { get; set; }
public bool IsLoggedIn { get; set; }
public int LineGroupID { get; set; }
internal LineGroup LineGroup { get; set; }
}
I am using these through an injected DB context, and can query each just fine without using navigation properties. I can also query the LineGroups and include the LineGroupMembers property just fine, like so:
var LineGroups = _context.LineGroups.Include(l => l.LineGroupMembers).ToList();
This load all of the line groups into a list that has a correctly working "LineGroupMembers" collection for each Line Group. However, if I try
var lineGroupMembers = _context.LineGroupMembers.Include(m => m.LineGroup).ToList();
I get "NullReferenceException" with no helpful details. Any ideas why the navigation property will work one way and not the other? There are no null values in either database table...
Make your navigation property public
public LineGroup LineGroup { get; set; }
If it is internal it won't be picked up by default by EF. You could also add explicit fluent mapping to force EF to recognize it as well.

Different behavior across domain objects for same properties in Entity Framework Core 2

I am new to .NET Core and using EF Core 2
My domain objects are all derived from a base class with some audit fields on it that get set on SaveChanges as needed:
(Simplified below)
public abstract class AuditableEntity
{
public DateTime CreatedOn { get; set; }
[ForeignKey("CreatedBy")]
public Guid? CreatedByWebUserId { get; set; }
public WebUser CreatedBy { get; set; }
public DateTime? UpdatedOn { get; set; }
[ForeignKey("UpdatedBy")]
public Guid? UpdatedByWebUserId { get; set; }
public WebUser UpdatedBy { get; set; }
public DateTime? DeletedOn { get; set; }
[ForeignKey("DeletedBy")]
public Guid? DeletedByWebUserId { get; set; }
public WebUser DeletedBy { get; set; }
}
On add-migration, I get the error:
Unable to determine the relationship represented by navigation property
'Address.CreatedBy' of type 'WebUser'. Either manually configure the
relationship, or ignore this property using the '[NotMapped]' attribute or by
using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Address is one of the classes derived from AuditableEntity:
(Simplified below)
public class Address : AuditableEntity
{
public Guid Id { get; set; }
public string Nickname { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string Address3 { get; set; }
public string City { get; set; }
public string StateProvince { get; set; }
public string PostalCode { get; set; }
public string CountryCode { get; set; }
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
}
However, I have several objects that use the same "agent and timestamp" pair pattern similar to the above that work just fine such as:
public DateTime? VerifiedOn { get; set; }
[ForeignKey("VerifiedBy")]
public Guid? VerifiedByWebUserId { get; set; }
public WebUser VerifiedBy { get; set; }
The error always comes from Address, and if I remove the base class from Address everything works fine (meaning, these fields get successfully applied to my 15+ other domain objects).
The issue seemingly stems from WebUser having a reference to Address:
(Simplified below)
public class WebUser : AuditableEntity
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string EmailAddress { get; set; }
public string Phone1 { get; set; }
public string Phone1Type { get; set; }
public string Phone2 { get; set; }
public string Phone2Type { get; set; }
[ForeignKey("AddressId")]
public Address Address { get; set; }
public Guid? AddressId { get; set; }
}
What is the correct way of creating these references prioritizing keeping the FK constraints (over keeping the ability to navigate)?
The problem is unrelated to the usage of a base class (the same will happen if you remove the base class, but copy its properties to Address class), but the multiple cross references between the two classes.
By convention EF Core tries to automatically "pair" navigation properties of the two entities in order to form a single relationship, which succeeds in most of the cases. However in this case the WebUser has Address type navigation property and Address class has WebUser type navigation property (actually 3).
Since all they have associated FK property via ForeignKey data annotation, EF Core should be able to correctly identify them as different one-to-many relationships, but it doesn't. Not only it fails with the exception in question, but also doesn't create FK relationships for the WebUser.
Everything works correctly if the base class contains only 1 WebUser type of navigation property, so I'm assuming thet unfortunately you are hitting some current EF Core bug.
As a workaround until they fixed it, I would suggest explicitly configuring the problematic relationships using fluent API, by overriding the OnModelCreating and adding the following code:
var auditableEntityTypes = modelBuilder.Model.GetEntityTypes().Where(t => t.ClrType.IsSubclassOf(typeof(AuditableEntity)));
var webUserNavigations = new[] { nameof(AuditableEntity.CreatedBy), nameof(AuditableEntity.DeletedBy), nameof(AuditableEntity.UpdatedBy) };
foreach (var entityType in auditableEntityTypes)
{
modelBuilder.Entity(entityType.ClrType, builder =>
{
foreach (var webUserNavigation in webUserNavigations)
builder.HasOne(typeof(WebUser), webUserNavigation).WithMany();
});
}
i.e. for each entity class that derives from AuditableEntity we explicitly configure the 3 WebUser reference navigation properties to be mapped to 3 separate one-to-many relationships with no inverse collection navigation properties. Once we do that, EF Core has no problem to correctly map the WebUser.Address FK association.

AutoMapper does not unproxy NHibernate entity

Consider this entity:
public class CondRule
{
public virtual decimal Id { get; set; }
public virtual string Name { get; set; }
public virtual CondRuleType RuleType { get; set; }
public virtual string Statement { get; set; }
}
and CondRuleType is:
public class CondRuleType
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
It is obvious that there is a one to one relation between CondRule and CondRuleType entities.
Also I have CondRuleDto:
public class CondRuleDto
{
public decimal Id { get; set; }
public string Name { get; set; }
public CondRuleType RuleType { get; set; }
}
I have mapped CondRule to CondRuleDto using AutoMapper:
Mapper.CreateMap<CondRule, CondRuleDto>();
When I call Session.Get to get CondRule by id and the map the result to CondRuleDto, AutoMapper does not resolve proxies (here RuleType).
Here is my code:
var condRule = Session.Get<CondRule>(id);
var condRuleDto = Mapper.Map<CondRuleDto>(condRule);
When I watch condRuleDto, RuleType property is a NHibernate proxy. I want AutoMapper to map RuleType proxy to a POCO. How to make this work?
PS: I have to mention that when I use query and use automapper's Project, it will result a list with no proxies (I know that Project make this happen. May be I need something like Project to use after Session.Get):
Session.Query<CondRule>().Project().To<CondRuleDto>().ToList()
Casts won't change the underlying object (i.e. your CondRuleType will be still a proxy even if you map its instance to another property of type CondRuleType).
It seems like you need to create a custom mapping where CondRule.RuleType is mapped creating a new instance of CondRuleType.

Categories