Nullable optional parameter - c#

I am using the entity framework 4 with edmx files and POCOs within an asp.net mvc application.
First of all I have a person class which is mapped to a table in the database.
public class Person
{
public Int32 ID{get;set;}
public string Name{get;set;}
public Int32? ParentID{get;set;}
}
Then in my service layer I have the following function to retrieve all persons. If a parentID is supplied the persons retrieved will be the ones with that parentID:
public List<Person> Get(int? parentPersonID = null)
{
var persons = Repository().GetAll(c => c.ParentID == parentPersonID);
}
Finally, the Repository() function returns an IRepository<Person> which contains the method:
public IQueryable<TModel> GetAll(Expression<Func<TModel, bool>> predicate = null)
{
var result = ObjectSet.AsQuaryable(); //ObjectSet is a ObjectSet<Person> instance
if (predicate != null)
result = result.Where(predicate);
return result;
}
Now, the problem is that if I pass null as parentPersonID to the service layer, so as Get(null). Enumeration yields no results. However if I modify the service layer code to:
public List<Person> Get(int? parentPersonID = null)
{
var persons = Repository().GetAll(null);
}
everything works as expected.
Any ideas why is that?
EDIT:
If i replace the service layer function code with:
var persons = Repository().GetAll(c => c.ParentID.Equals(parentPersonID));
instead of:
var persons = Repository().GetAll(c => c.ParentID == parentPersonID);
it works as expected - the first line retrieves records from the DB whereas the second one does not.
I am still curious as to what is the difference in the Equals() and == in this case.

I suspect it's to do with how equality is being handled. Try this:
public List<Person> Get(int? parentPersonID = null) {
var persons = Repository().GetAll(parentPersonID == null ?
c => !c.ParentID.HasValue :
c => c.ParentID == parentPersonID);
...
}
This will change the predicate to be an explicit nullity check when you pass in a parentPersonID of null, rather than making it just match the value you've passed in. There may well be a more elegant way of expressing it, but it's worth at least trying that to start with.
(I assume that if you specify a parentPersonID of null you want to get all the people with a null parentPersonID, not just all the people... that would be a different change.)

Related

EF Include(Expression)

I am trying to build generic method which would look simething like this:
public static IQueryable<TModel> IncludeByUserCondition<TModel, TIncludable>(this IQueryable<TModel> query, Func<TModel, IQueryable<TIncludable>> includes, List<int> userIDs)
where TModel : class
where TIncludable: class
{
Expression<Func<TModel, object>> result = x => includes(x);
if(typeof(ASqlBase).IsAssignableFrom(typeof(TIncludable)))
{
result = x =>
includes(x)
.Select(prop => prop as ASqlBase)
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
.Select(prop => prop as TIncludable);
}
query = query.Include(result);
return query;
}
This method would allow me to centrally check if user can read navigation property's value and, if so, include it in result. My applications read rights are conceived in hierarchical way: logged user can read his records and records of all users he had added to the system. Because of that, I cannot determine all visible records in compile-time and, thus, cannot use different database contexts for different groups of users. Also, since this is only one of many ways for filtering data, unfortunately I cannot make use of Global Filters.
I am trying to call the above method like this:
qry = qry.IncludeByUserCondition<AllocatedFund, AllocatedFundDetailPaymentMade>(p => p.AllocatedFundDetailPaymentsMade.AsQueryable(), allowedUserIDs);
However, when I try to invoke it in run-time, I get the following exception:
The expression 'Invoke(__includes_0, x).Select(prop => (prop As ASqlBase)).Where(prop => ((prop.DeleteDate == null))).Where(prop => True).Select(prop => (prop As AllocatedFundDetailPaymentMade))' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations.
On another hand, when I try to run query manually, everything works fine:
qry = qry.Include(p =>
p.AllocatedFundDetailPaymentsMade
.AsQueryable()
.Where(prop =>
prop.DeleteDate == null
)
.Where(prop =>
userIDs != null && userIDs.Count > 0 ? userIDs.Contains(prop.IdentityUnitID) : true
)
Since I have more than just one navigation property to include in this manner (and I have to perform query in similar manner for all 30+ other models I use in my application), I wouldn't want to manually write those where clauses in every query.
Does anybody know the solution for this problem? Any help would be kindly appreciated.
EDIT:
ASqlBase is just base, abstract class from which some other models interit (although not all of them - ie. User model does not inherit from ASqlBase).
ASqlBase looks like this:
public abstract class ASqlBase
{
[Key]
public int ID { get; set; }
[Required]
public int UserID { get; set; }
[ForeignKey("UserID")]
public virtual User User { get; set; }
public DateTime? DeleteDate { get; set; }
}
I plan to use that function to get data and then display it in report. I'm giving example for Person, then the method call would look something like this:
var qry = dbContext.Person.IncludeByUserCondition<Person, Athlete>(p => p.Athletes.AsQueryable(), athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Employee>(p => p.Employees.AsQueryable(), employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition<Person, Student>(p => p.Students.AsQueryable(), studentAllowedUserIDs);
Person model looks something like this:
public class Person : ASqlBase
{
...
public virtual ICollection<Athlete> Athletes { get; set; }
public virtual ICollection<Employee> Employees { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
All of the above models: Athlete, Employee and Student inherit from ASqlBase
EDIT 2:
Sorry for bad method naming, method should be called IncludeByUserCondition, not IncludeMultiple (as it was named before).
A little bit simplified usage. You do not need to call AsQueryable() and explicitly specify generic parameters:
var qry = dbContext.Person.IncludeByUserCondition(p => p.Athletes, athleteAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Employees, employeeAllowedUserIDs);
qry = qry.IncludeByUserCondition(p => p.Students, studentAllowedUserIDs);
And realization:
public static class IncludeExtensions
{
public static IQueryable<TModel> IncludeByUserCondition<TModel, TRelated>(this IQueryable<TModel> query,
Expression<Func<TModel, IEnumerable<TRelated>>> collectionProp, List<int> userIDs)
where TModel : class
where TRelated : ASqlBase
{
var relatedParam = Expression.Parameter(typeof(TRelated), "r");
// r.DeleteDate == null
var filterPredicate = (Expression)Expression.Equal(
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.DeleteDate)),
Expression.Constant(null, typeof(DateTime?)));
if (userIDs?.Count > 0)
{
// r.DeleteDate == null && userIDs.Contains(r.UserID)
filterPredicate = Expression.AndAlso(filterPredicate,
Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(int) },
Expression.Constant(userIDs),
Expression.PropertyOrField(relatedParam, nameof(ASqlBase.UserID))));
}
// r => r.DeleteDate == null && userIDs.Contains(r.UserID)
var filterLambda = Expression.Lambda(filterPredicate, relatedParam);
// p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID))
var transformedProp = Expression.Lambda(Expression.Call(typeof(Enumerable), nameof(Enumerable.Where),
new[] { typeof(TRelated) }, collectionProp.Body, filterLambda), collectionProp.Parameters);
// query.Include(p => p.Navigation.Where(r => r.DeleteDate == null && userIDs.Contains(r.UserID)))
var includeExpression = Expression.Call(typeof(EntityFrameworkQueryableExtensions),
nameof(EntityFrameworkQueryableExtensions.Include),
new[] { typeof(TModel), typeof(IEnumerable<TRelated>) },
query.Expression,
Expression.Quote(transformedProp));
// instantiate new IQueryable<TModel>
var resultQuery = query.Provider.CreateQuery<TModel>(includeExpression);
return resultQuery;
}
}

How can I tell why/how some LINQ queries work with each other and others don't?

Take the following EF Class:
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IEnumerable<Property> Property { get; set; }
}
public class Property
{
public int ID { get; set; }
public string Name { get; set; }
public bool Lock { get; set; }
public Person Person { get; set; }
public int PersonID
}
I can pretty much make everything work as expected - including a delete action for Person that also deletes all their property. However, as my code gets more complicated, I want to make the logic slightly more advanced.
In the above example, we have something elsewhere that will set the bool lock for property. In this case, I want to disable delete on person when any property for that person has a lock of true.
The default Delete controller code has:
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
if (person== null)
{
return NotFound();
}
return View(person);
}
And the Delete confirm has:
public async Task<IActionResult> DeleteConfirmed(int id)
{
var person= await _context.people.FindAsync(id);
_context.people.Remove(person);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
I know the code to do what I want is:
foreach (var item in person.Property)
{
if item.locked==true
return("error")
}
Now the fun stars! - The old EF4 virtual keyword on properties I'm used to doesn't work - so, I can't iterate over the property because it is currently null. in most instances, I have to call .include()
On the first delete, this modifies:
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
to
var person = await _context.People.Include(x=>x.property)
.FirstOrDefaultAsync(m => m.ID == id);
which seems to work fine.
However, the second one:
var person = await _context.people.FindAsync(id);
doesn't seem to work. The moment I put the .Include in, it states error CS1061 that there is no definition for FindAsync.
In all honesty, I am not too sure what the need is for two different ways of looking at an ID in the first place... I can only assume that when looking for an ID in the first delete that may not exist, firstordefault is the best and when confirming a delete, find is best.... however, this is what the scaffolding does and I don't feel I know enough to question this.
I however want to be a better developer and would love to understand what is wrong with the code and for future, how do I know what can be combined and what can't as I don't feel I am learning here, I am just randomly trying different things until I find one combination that works.
A few things:
I'd consider checking whether the person is Locked before enabling a Delete button, or immediately on clicking the delete button rather than on confirming a delete.
With this code:
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
if (person== null)
return NotFound();
return View(person);
Entities should represent data state, not view state. Returning entities to the view will lead to problems. If lazy loading is supported/enabled this can trigger performance issues when lazy-loads get triggered by serialization, it can also lead to errors due to cyclical references. It exposes more information about your data structure, and data in general that the client does not need (more data over the wire and more information for hackers). Instead, leverage a ViewModel POCO class containing just the data your view needs and use .Select() to populate it.
Next, avoid the crutch of FirstOrDefault. This can lead to unintended bugs remaining hidden. If there is 1 entity expected, use Single or SingleOrDefault. Your application should handle exceptions gracefully and impartially. If someone sends an invalid ID, fail and terminate the session. Basically do not trust the client not to tamper.
var person = await _context.People
.Select(x => new PersonViewModel
{
PersonId = x.ID,
Name = x.FirstName + " " + x.LastName,
// etc.
}).SingleAsync(x => x.ID == id);
return View(person);
When checking data state on the confirm, you receive and ID and want to confirm before issuing the delete, you can query the required detail, but then for a delete you don't need the entire entity provided you trust the ID. Something like this isn't needed:
foreach (var item in person.Property)
{
if item.locked==true
return("error")
}
Instead:
var isLocked = context.People.Where(x => x.ID == id)
.Select(x => x.Property.Any(p => p.isLocked))
.Single();
This will throw if the person ID isn't found, and return a Bool True of False if any of the Property entries for that person are locked.
From there you can use a simple "trick" to delete an entity without first loading it:
if (!isLocked)
{
var person = new Person { ID = id };
context.People.Attach(person);
context.People.Remove(person);
context.SaveChanges();
}
Alternatively if you want to load the Person to have access to other properties, such as to create an audit record or may want to display info anyways as part of the error message, then you can substitute the above examples with:
var personData = context.People.Where(x => x.ID == id)
.Select(x => new
{
Person = x,
IsLocked = x.Property.Any(p => p.isLocked))
}).Single();
if (!personData.isLocked)
{
context.People.Remove(personData.Person);
context.SaveChanges();
}

.net core 2 Format API Response with deep properties

I am building an application that consists of only an API using .net core 2.1.
I have a parent class (Dad) which owns only a single child (Kid). I am looking for the most efficient way to format the JSON response of my controller; for developers who will integrate my API to their applications.
public class Dad
{
public long Id{get;set;}
public string Name{get;set;}
public Kid OnlyChild {get;set;}
}
public class Kid
{
public long Id{get;set;}
public string FirstName{get;set;}
public string LastName{get;set;}
public string Useless{get;set;}
}
Currently I am doing something like this in the controller:
[HttpGet("{id}")]
public async Task<IActionResult> GetDad([FromRoute] long id)
{
dynamic DadResponse = _context.Dads
.Where(o => o.Id == id)
.AsNoTracking()
.Select(p => new
{
Dad = p.Name,
Kid = string.Format("{0} {1}", p.Kid.FirstName, p.Kid.LastName)
}).FirstOrDefault();
return Ok(DadResponse);
}
The upside with this approach is that:
the resulting DadResponse object has the format I want for my
API.
the resulting MySQL query generated by EF Core will be
optimized and only select Dad.Name, Kid.FirstName and Kid.LastName.
The downside is that if Kid is null, it will generate an exception.
What is the best way around this; maybe I am using a wrong approach all together. I tried to use JsonIgnore attributes in my models but each of my controllers might need to return slightly different properties (e.g. GET /Kids will return all the kids with their Id, whereas GET /Dads may return only the format described above).
Update:
Ideally I would like to have Kid return null value if Dad doesn't have a Kid, but I cannot do something like this:
Kid = (Kid == null ? null : string.Format("{0} {1}", p.Kid.FirstName, p.Kid.LastName))
I have tried to dynamically update the value after the select, using the following:
dynamic DadResponse = _context.Dads
.Where(o => o.Id == id)
.AsNoTracking()
.Select(p => new
{
Dad = p.Name,
Kid = p.Kid
}).FirstOrDefault();
DadResponse.Kid = (DadResponse.Kid == null ? null : string.Format("{0} {1}", DadResponse.Kid.Firstname, DadResponse.Kid.Lastname);
return Ok(DadResponse);
But that throws another exception.
Your query is an EF query that will be translated to SQL. Null propagation can't be translated which is why you get that error.
You don't have to format the string in the EF query though, you can load the data you want and use another LINQ to Objects query to map them to their final form. If you load only a single object, you can return a new anonymous type:
For example :
var data = _context.Dads
.AsNoTracking()
.Where(o => o.Id == id)
.Select(p => new {
Dad = p.Name,
Kid = new {p.Kid?.FirstName, p.Kid?.LastName}
})
.FirstOrDefault();
var dadResponse = new {
data.Dad,
Kid= $"{data.Kid.FirstName} {data.Kid.LastName}"
};
return Ok(dadResponse);
If you don't want to return the Kid element at all, you can simply omit it from the result:
if (data.Kid.FirstName==null && data.Kid.LastName==null)
{
return Ok(new {Dad=data.Dad});
}
else
{
...
}
Of course, someone could say that since we don't care about the Kid property, we could just return the KidFirstName and KidLastName as separate properties, making the code a bit simpler :
var data = _context.Dads
.AsNoTracking()
.Where(o => o.Id == id)
.Select(p => new {
Dad = p.Name,
KidFirstName = p.Kid?.FirstName
KidLastName = p.Kid?.LastName
})
.FirstOrDefault();
if (data.KidFirstName==null && data.KidLastName==null)
{
return Ok(new {data.Dad});
}
else
{
return Ok(new {data.Dad,Kid=$"{data.KidFirstName} {data.KidLastName}");
}

EF6 using custom property in a linq query

I have a class which has the following property:
[NotMapped]
public string Key
{
get
{
return string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
}
}
The local sequence number is a computed integer backed by a cache in form of a concurrent dictionary.
I wish to use the Key property above in a LINQ query but get the exception:
The specified type member 'Key' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.
I understand why I'm getting this error, but I'm not too sure about how to remedy it. Currently, the Key property is providing a nice encapsulation over my class which I don't want to part with. Any suggestions in terms of libraries, or simple patterns to get around this?
Edit: Here's the query which is throwing the exception:
db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);
The DelegateDecompiler package https://github.com/hazzik/DelegateDecompiler handles this type of scenario.
Decorate your property with the Computed attribute, then queries like the following should work if you add the Decompile method:
db.Cars.Decompile().SingleOrDefault(c => c.Id == id && c.Key == key)
There are numerous third party packages that can solve this problem. I also believe that there are methods in EF.Core that can help, however, I will suggest 2 "pure Entity Framework 6" solutions.
Execute your query in two parts - the SQL part, then the "in code" part.
db.Cars.Where(c => c.Id == id).ToList().SingleOrDefault(c => c.Key == key)
this will still keep your logic encapsulated in the class, but you do not get the benefit of the SQL execution.
What I like to call the "projector" pattern. This one is a bit more long-winded.
Essentially, you create a "view" of the EF POCO that represents a data-transfer-object. it has the properties you need for your view, and also determines how to project the data from the database to the view.
// Poco:
public class Car {
public int Id {get;set;}
public string LocalSequenceNumber {get;set;}
public int ProcessId {get;set; }
public virtual Process Process {get;set;}
// ...
}
public class Process {
// ...
}
// View+Projector:
public class CarView
{
public int Id {get;set;}
public string Color {get;set;}
public string Key {get;set;}
public static Expression<Func<Car, CarView>> Projector = car => new CarView {
Id = car.Id,
Color = car.Color,
Key = car.Process.Name + " " + car.LocalSequenceNumber
}
}
// calling code
var car = db.Cars.Select(CarView.Project).SingleOrDefault(cv => cv.Id == id && cv.Key == key)
This will evaluate all code on the database, whilst encapsulating your business logic in code.
Alas you forgot to tell us what Process.Name and LocalSequenceNumber are. From the identifiers it seems that they are not part of your Cars, but values in your local process. Why not calculate the Key before your query?
var key = string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);
If, on the other hand, Process.Name or LocalSequenceNumber are Car properties, you'll have to change the IQueryable.Expression that is in your LINQ query using only properties and methods that can be translated by your IQueryable.Provider into SQL.
Luckily, your Provider knows ToSTring() and the concept of string concatenation So you can use that
As you are using property Key in a Queryable.Where, I suggest extending IQueryable with a function WhereKey. If extension functions are a bit magic for you, see Extension Methods Demystified
public static IQueryable<Car> WhereKey(this IQueryable<Car> cars, int id, string key)
{
return cars.Where(car => car.Id == id
&& key == car.Process.Name.ToString() + "_" + car.LocalSequenceNumber.ToString());
}
Usage:
int carId = ...
string carKey = ...
var result = myDbContext.Cars
.WhereKey(carId, carKey)
.FirstOrDefault();
Consider creating a WhereKey that only checks the key. The concatenate with a Where that selects on Id.
var result = myDbContext.Cars
.Where(car => car.Id == id)
.WhereKey(carKey)
.FirstOrDefault();
If either Process.Name or LocalSequenceNumber is not a part of Car, add it as a parameter. You get the gist.
Consider creating a WhereKey that only checks the key. The concatenate with a Where that selects on Id.
If desired, you can create a WhereKeyFirstOrDefault(), but I doubt whether this would be of much use.

Only include what is included Entity Framework

I am doing a big database call for a shopping cart. It includes many relations, all specified with the .Include() method.
Now I only want EF to include what I have specified. When I include a collection, it automatically loads the collection's relations.
So I have a ShoppingCart, the shopping cart has a collection if ShoppingCartProducts and that one has a relation back to ShoppingCart and to Product.
So I want to include product, but not the shopping cart again, so I do:
IQueryable<ShoppingCart> query = DbContext.ShoppingCarts
.Include(p => p.ShoppingCartProducts)
.Include(p => p.ShoppingCartProducts.Select(x => x.Product))
Later I execute a .FirstOrDefault() which executes the query. Debugging through this, it has also included ShoppingCart within each ShoppingCartProducts.
This sounds a bit small, but it is actually this way throughout the application. New architecture turns entity objects into models with different static methods and extensions. Eventually causing an StackoverflowException because it recursively includes it's relations.
So how do I only Include what I have included?
I've turned LazyLoadingEnabled to false, and ProxyCreationEnabled to false. And my collections/reations are not marked with virtual.
Checked these answers:
DBContext lazyloadingenabled set to true still loads related entities by default
It is true about the include on collections, but once a collection is included, that collection will load all other relations (I guess)
Entity Framework with Proxy Creation and Lazy Loading disabled is still loading child objects
Almost same question, yet not an good answer, only an explanation
EF 6 Lazy Loading Disabled but Child Record Loads Anyway
Using detached didn't help.
Edit:
As Evk mentioned, this has something to do with EF automatically filling up the blanks for already known relations. Question is now actually how to turn this off.
Edit 2:
So after an answer from Evk and my own workaround, we learn that these solutions don't solve the big picture. Let me try to explain:
These extensions and ConvertToModel methods are implemented in every repository and calling each other whenever it has a relation to it. The concept is actually great: Just convert to a model if you have the relation, if you have not, don't do anything. Yet because of this EF 'bug' I learn that all relations that are known inserted everywhere.
Here is an example where our solutions don't work. This for the case the code would call ConvertToModel for the ShoppingCart first, then the rest. But of course it could be visa-versa.
ShoppingCartRepository
public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
{
if (entity == null) return null;
ShoppingCartModel model = new ShoppingCartModel
{
Coupons = entity.ShoppingCardCoupons?.SelectShoppingCouponModel(typeof(ShoppingCart)),
Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),
};
return model;
}
ShoppingCartProductRepository
public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
{
bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);
return source.Select(x => new ShoppingCartProductModel
{
ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
ShoppingCartCoupons = includeRelations && objSource != typeof(ShoppingCartCoupon) ? x.ShoppingCartCoupons?.SelectShoppingCouponModel(typeof(ShoppingCartProduct)) : null,
});
}
ShoppingCartCouponRepository
public static IEnumerable<ShoppingCartCouponModel> SelectShoppingCouponModel(this IEnumerable<ShoppingCartCoupon> source, Type objSource = null)
{
bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartCoupon>);
return source.Select(x => new ShoppingCartCouponModel
{
ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
ShoppingCartProduct = includeRelations && objSource != typeof(ShoppingCartProductModel) ? ShoppingCartProductRepository.ConvertToModel(x.ShoppingCartProduct) : null
});
}
When you study it, you will see it can go from ShoppingCart to ShoppingCartProduct to ShoppingCartCoupon back to ShoppingCart.
My current workaround will be to figure out the aggregate roots and choose which one needs which one. But I rather have an elegant solution to solve this. Best would be to prevent EF from loading those known relations, or somehow figure out if a property was loaded that way (reflection?).
As stated in comments, that's default behavior of entity framework and I don't think it can be changed. Instead, you can change your code to prevent stackoverflow exceptions. How to do that nicely is very dependent on your codebase, but I'll provide one sketch. In the sketch above I use other entity names (because I always check if my code samples at least compile before posting them here):
public static partial class Ex {
public static CodeModel ConvertToModel(Code entity) {
if (entity == null) return null;
CodeModel model = new CodeModel();
var map = new Dictionary<object, object>();
map.Add(entity, model);
model.Errors = entity.Errors?.SelectShoppingCartProductModel(map);
return model;
}
public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source, Dictionary<object, object> map = null) {
bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
return source.Select(x => new ErrorModel {
Code = includeRelations ? (map?.ContainsKey(x.Code) ?? false ? (CodeModel) map[x.Code] : ConvertToModel(x.Code)) : null,
// other such entities might be here, check the map
}).ToArray();
}
}
Another option is to store current model in thread local variable. If you call some ConvertToModel method and this thread local variable is not null - that means this method has been called recursively. Sample:
public static partial class Ex {
private static readonly ThreadLocal<CodeModel> _code = new ThreadLocal<CodeModel>();
public static CodeModel ConvertToModel(Code entity) {
if (entity == null) return null;
if (_code.Value != null)
return _code.Value;
CodeModel model = new CodeModel();
_code.Value = model;
model.Errors = entity.Errors?.SelectShoppingCartProductModel();
// other setters here
_code.Value = null;
return model;
}
public static ErrorModel[] SelectShoppingCartProductModel(this IEnumerable<Error> source) {
bool includeRelations = source.GetType() != typeof(DbQuery<Error>); //so it doesn't call other extensions when we are a db query (linq to sql)
return source.Select(x => new ErrorModel {
Code = includeRelations ? ConvertToModel(x.Code) : null,
}).ToArray();
}
}
If you implement this in all your ConvertToModel methods - there is no need to pass any parameters or change other parts of your code.
This solution checks if the source object type is not equal to the one we are calling ConvertToModel for.
public static ShoppingCartModel ConvertToModel(ShoppingCart entity)
{
if (entity == null) return null;
ShoppingCartModel model = new ShoppingCartModel
{
...
Products = entity.ShoppingCartProducts?.SelectShoppingCartProductModel(typeof(ShoppingCart)),
};
return model;
}
and the SelectShoppingCartProductModel extension:
public static partial class Ex
{
public static IEnumerable<ShoppingCartProductModel> SelectShoppingCartProductModel(this IEnumerable<ShoppingCartProduct> source, Type objSource = null)
{
bool includeRelations = source.GetType() != typeof(DbQuery<ShoppingCartProduct>);//so it doesn't call other extensions when we are a db query (linq to sql)
return source.Select(x => new ShoppingCartProductModel
{
....
ShoppingCart = includeRelations && objSource != typeof(ShoppingCart) ? ShoppingCartRepository.ConvertToModel(x.ShoppingCart) : null,
});
}
}
Yet this probably doesn't solve the entire problem. If you have another entity, let's say AdditionalCosts inside the ShoppingCart, that also has a reference to ShoppingCartProduct, it will still 'spin around'. If someone has a solution for this it would be great!
ShoppingCart -> ConvertToModel(shoppingCart) -> SelectAdditionalCostsModel -> ShoppingCartProduct -> ConvertToModel(shoppingCartProduct) -> ShoppingCart -> ConvertToModel(shoppingCart). And so on..

Categories