How do I do a left/right join using DbSet - entity-framework-core

I have the following 3 models:
class Owner
{
public Guid Id { get; set; }
public IList<AccessShare> AccessShares { get; set; } = new List<AccessShare>()
}
class AccessShare
{
public Guid OwnerId { get; set; }
public Guid AccessorId { get; set; }
}
class File
{
public Guid OwnerId { get; set; }
public Owner Owner { get; set; }
public string Filename { get; set; }
}
The purpose of the system is assign ownership over files to one user, and to only allow other users to see those entities when there is an AccessShare from Accessor to Owner.
I have the following code that I'm using to bring back a file:
Guid fileId = <code that gets the accessor id>
Guid accessorId = <code that gets the accessor id>
File? file = DBContext
.Set<File>()
.Where(e => e.Id == fileId)
.Where(e => e.Owner.Id == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId))
.Include(e => e.Owner)
.Include(e => e.Owner.AccessShares)
.FirstOrDefault();
The issue I'm getting is that if null is returned, I don't know it that's because there isn't a File entity with the given id, or if there isn't an access share that allows access.
If this was raw SQL I'd do a left join from Owners to AccessShares with the above condition, this would always give me back the file/owner if found, and then optionally any access shares that meet the criteria.
I can find examples of how to do it in SQL and in Linq, but I can't find any examples using the DbSet fluid style.

From what you describe the part you are having issue with is:
.Where(e => e.Owner.Id == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId))
This will only return the desired file If the file is owned by the user, or has an access share.
Now if you want to return a File if it exists and provide an indication to the user whether they have permissions to access it, that could be done via projection rather than returning entities:
For example if I have a File ViewModel/DTO:
public class FileViewModel
{
public int FileId { get; set; }
// any other fields about the file I might display/use...
public bool UserHasAccess { get; set; }
public bool UserIsOwner { get; set; }
}
then I can query and populate these computed values via EF:
FileViewModel? file = DBContext
.Set<File>()
.Where(e => e.Id == fileId)
.Select(e => new FileViewModel
{
FileId = e.FileId,
// other fields...
UserHasAccess = e.OwnerId == accessorId || e.Owner.AccessShares.Any(a => a.AccessorId == accessorId),
UserIsOwner = e.OwnerId == accessorId
}).SingleOrDefault();
This will return a file if it exists, and includes details that your view/logic can use to determine if the file can be accessed by the current user, and/or is owned by the current user. Note that we remove the extra Where condition, and we don't need Include to access those related members when projecting.
Alternatively computed properties like this can be added to the File entity to do the same thing, however for these properties to function would require that the Owner and AccessShares are eager loaded (/w Include) or can be lazy loaded, accepting the potential performance pitfalls that can come with that. IMHO Projection /w Select is almost always preferable as it can also improve the resource footprint and performance of the querying.

Related

.NET Core - join 2 tables with AsQueryable

I have two nested classes: Partner contains a Company (that has a field "Name")
I do a search by Id on the partner's Id
I want to do a search on the company's "Name" field
here is my poco:
public class Partner
{
[Required]
public int? Id { get; set; }
[Required]
public Company Company { get; set; }
using AsQueryable, I can then stack filters one by one
I try to have a query that joins the second table to do a search on that entity's name field
public DbSet<Partner> Partners { get; set; }
...
var data = _context.Partners.AsQueryable();
if (partnersSearch.SearchById != null)
{
data = data.Where(p => p.Id == partnersSearch.SearchById.GetValueOrDefault());
}
if (partnersSearch.SearchByName != null)
{
data = data.Include(a => a.Company.Select(b => b.Name = partnersSearch.SearchByName));
but for the join between the tables, the last line cannot compile
it complains that Company does not contain a definition of has no Select
what am I doing wrong ?
thanks for helping me on this
If you try a where after your include. Does that help?
data.Include(a => a.Company).Where(partner=>partner.Company.Name.equals(partnersSearch.SearchByName))

Entity Framework - per distinct string property only have one possible "true" value for other property

Imagine having an entity like this example:
public class Thing
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
How can I add a contraint-ish so that only ONE of all the Things with a certain name can have true for IsActive?
In other words, we can have multiple Things with the same name, but at any given time only one can have an IsActive which is true - the others need to have false for IsActive. So if we want to add or update one, it needs to check if the new value is true for IsActive that it won't make a "conflict".
Is this somehow possible?
One option would be to adopt actions for changing entity state rather than public setters. For instance, in most cases my entities reside in a Domain assembly along with the DbContext and things like Repository classes. "Things" would reside under a container entity to be uniquely associated.
public class Thing
{
public int Id { get; internal set;}
public string Name { get; internal set; }
public bool IsActive { get; internal set; }
}
public class Container
{
public int Id { get; internal set; }
public virtual ICollection<Thing> Things { get; internal set; } = new List<Thing>();
public void AddThing(string name, bool isActive = true)
{
var existingActiveThing = Things.SingleOrDefault(x => x.Name == name && x.IsActive);
if(isActive && existingActiveThing != null)
existingActiveThing.IsActive = false;
var newThing = new Thing { Name = name, IsActive = isActive };
Things.Add(newThing);
}
public void ActivateThing(int thingId)
{
var thing = Things.Single(x => x.Id == thingId);
if(thing.IsActive)
return; // Nothing to do.
var existingActiveThing = Things.SingleOrDefault(x => x.Name == thing.Name && x.IsAcitve);
if (existingActiveThing != null)
existingActiveThing.IsActive = false;
thing.IsActive = true;
}
// And so forth for instance Renaming a Thing or other allowed actions.
}
Container could be a containing entity, or these actions could be encapsulated in a domain level service class initialized with the collection of Things or access to the DbContext if Things are a top-level entity. The setters are marked as Internal to require the use of a domain level method to mutate state rather than arbitrary updates via the setters. This technique can be useful where you want to enforce validation across multiple entities or multiple fields to ensure the entity state at every point in time is valid. (I.e. updating address fields as a set of values to be validated rather than 1 field at a time, where you can change Country or such leaving the entity in an invalid combination of values that cannot pass validation)

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

EF code first MVC4 - ArgumentNullException on edit - what am I doing wrong here?

Let's say I have 3 models:
[Table("UserProfile")]
public class UserProfile //this is a standard class from MVC4 Internet template
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int UserProfileId { get; set; }
[ForeignKey("UserProfileId")]
public virtual UserProfile UserProfile { get; set; }
}
Now, I'm trying to edit Post
[HttpPost]
public ActionResult Edit(Post post)
{
post.UserProfileId = context.UserProfile.Where(p => p.UserName == User.Identity.Name).Select(p => p.UserId).FirstOrDefault();
//I have to populate post.Category manually
//post.Category = context.Category.Where(p => p.Id == post.CategoryId).Select(p => p).FirstOrDefault();
if (ModelState.IsValid)
{
context.Entry(post.Category).State = EntityState.Modified; //Exception
context.Entry(post.UserProfile).State = EntityState.Modified;
context.Entry(post).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
And I'm getting ArgumentNullException.
Quick look into debug and I can tell that my Category is null, although CategoryId is set to proper value.
That commented out, nasty-looking trick solves this problem, but I suppose it shouldn't be there at all. So the question is how to solve it properly.
I would say it's something with EF lazy-loading, beacuse I have very similar code for adding Post and in debug there is same scenerio: proper CategoryId, Category is null and despite of that EF automagically resolves that Post <-> Category dependency, I don't have to use any additional tricks.
On edit method, EF has some problem with it, but I cannot figure out what I'm doing wrong.
This is working as intended. Your Post object is not attached to the Context, so it has no reason to do any lazy loading. Is this the full code? I don't understand why you need to set Category as Modified since you're not actually changing anything about it.
Anyway, I recommend you query for the existing post from the Database and assign the relevant fields you want to let the user modify, like such:
[HttpPost]
public ActionResult Edit(Post post)
{
var existingPost = context.Posts
.Where(p => p.Id == post.Id)
.SingleOrDetault();
if (existingPost == null)
throw new HttpException(); // Or whatever you wanna do, since the user send you a bad post ID
if (ModelState.IsValid)
{
// Now assign the values the user is allowed to change
existingPost.SomeProperty = post.SomeProperty;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
This way you also make sure that the post the user is trying to edit actually exists. Just because you received some parameters to your Action, doesn't mean they're valid or that the post's Id is real. For example, some ill intended user could decide to edit posts he's not allowed to edit. You need to check for this sort of thing.
UPDATE
On a side note, you can also avoid manually querying for the current user's Id. If you're using Simple Membership you can get the current user's id with WebSecurity.CurrentUserId.
If you're using Forms Authentication you can do Membership.GetUser().ProviderUserKey.

How to not assign an id when id is a fk with a custom id generator in ef

I have a project where I'm using EF5, I made a custom Guid Generator and I have an override of the SaveChanges method to assign the ids of my entities.
Everything is working fine except in one case: when the ID of one entity is a FK to another ID of another entity.
A little bit of code to explain the problem:
I have two entities I cannot change:
public class FixedEntityA
{
public Guid Id { get; set;}
public string SomeText { get; set; }
}
public class FixedEntityB
{
public Guid Id { get; set;}
public int OneInt { get; set; }
}
In my project I have an entity defined like this:
public class ComposedEntity
{
public Guid Id { get; set;}
public FixedEntityA FixedA { get; set; }
public FixedEntityB FixedB { get; set; }
public double OneDouble { get; set; }
}
The relationships are:
ComposedEntity may have 0 or 1 FixedEntityA
ComposedEntity may have 0 or 1 FixedEntityB
The constraints on the id are:
The Id of FixedEntityA is a FK pointing to the Id of ComposedEntity
The Id of FixedEntityB is a FK pointing to the Id of ComposedEntity
The mapping class are:
public ComposedEntity(): EntityTypeConfiguration<ComposedEntity>
{
HasOptional(fea => fea.FixedA).WithRequired();
HasOptional(feb => feb.FixedB).WithRequired();
}
Here is my SaveChanges override:
foreach (var entry in ChangeTracker.Entries<IEntity>().Where(e => e.State == EntityState.Added))
{
Type t = entry.Entity.GetType();
List<DatabaseGeneratedAttribute> info = t.GetProperty("Id")
.GetCustomAttributes(typeof (DatabaseGeneratedAttribute), true)
.Cast<DatabaseGeneratedAttribute>().ToList();
if (!info.Any() || info.Single().DatabaseGeneratedOption != DatabaseGeneratedOption.Identity)
{
if (entry.Entity.Id == Guid.Empty)
entry.Entity.Id = (Guid) _idGenerator.Generate();
}
}
return base.SaveChanges();
This code works fine everywhere for all kind of relationships except in this case, I am missing a test to make sure I'am not setting an id on id that are foreign keys, and I have no clue on how to check if an Id is a FK...
Here is a sample object where this code fails:
var fea = new FixedEntityA();
var feb = new FixedEntityB();
var composedEntity = new ComposedEntity();
composedEntity.FixedA = fea;
composedEntity.FixedB = feb;
If you insert the whole graph, all three objects are marked as Added and all Ids are default.
The problem is, with the current SaveChanges method, I will go through all object with the Added state in the change tracker and I will assign an Id to all entity with a default Guid and break my FK constraints.
Thanks in advance guys!
Here is some code that will get the FK properties for a given type (it's horrible I know). Should be simple enough to plug this into your code.
var typeName = "Category";
var fkProperties = ((IObjectContextAdapter)db)
.ObjectContext
.MetadataWorkspace
.GetItems<AssociationType>(DataSpace.CSpace)
.Where(a => a.IsForeignKey)
.Select(a => a.ReferentialConstraints.Single())
.Where(c => c.FromRole.GetEntityType().Name == typeName)
.SelectMany(c => c.FromProperties)
.Select(p => p.Name);