How to reference an entity from different entity types in EF Core - entity-framework-core

In my application I need to model the concept of a Link. It contains a name, a description and of course, a URL.
For the sake of simplicity, let´s assume I have 2 entities that can have many links: companies and projects.
If I add an ICollection of Link (generics) to the Company entity, EF will add a FK from Link to Company in the Link table. The same will happen if I add an ICollection of Link to the project entity.
I want the Link table to be completely agnostic of "who" might be referencing its records, and at the same time, being able to reference Link objects from wherever they are needed.
Is that possible at all?
Thanks!

The only way I know is through the use of a Principal Key. I'll use Link and Company in this example. The models as you indicated:
public class Link
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string URL { get; set; }
public string CompanyName { get; set; }
}
I added CompanyName, more on that later.
public class Company
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Link> Links { get; set; }
}
So as you may have discovered, even if there's no integer FK property explicitly defined in the child class, EF Core will still create it in the database anyway.
It's not really possible (as far as I know) to have a table that is completely unaware of its relationship with others. However, with principal keys you can set the FK to reference a property in the parent class other than the primary key, and for that purpose I added CompanyName to Link. It will reference the Name property in Company and use that to fill in its value. Here's the mapping you need to do in the DbContext class:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Link>()
.HasOne<Company>()
.WithMany(c => c.Links)
.HasForeignKey(l => l.CompanyName)
.HasPrincipalKey(c => c.Name);
}
Here's the resulting database schema after applying the migration (SQL Server):
This is the data I used to test it out:
var newCompanies = new List<Company>()
{
new Company()
{
Name = "EF Core Enterprises",
Links = new List<Link>()
{
new Link()
{
Name = "Introduction",
Description = "The EF Core 101",
URL = "https://efcore.com/intro"
},
new Link()
{
Name = "Mapping",
Description = "Mapping classes to database objects",
URL = "https://efcore.com/mapping"
},
new Link()
{
Name = "Change Tracker",
Description = "Deep dive into EF Core's object tracking mechanism",
URL = "https://efcore.com/changetracker"
}
}
},
new Company()
{
Name = "ASP.NET Core Inc.",
Links = new List<Link>()
{
new Link()
{
Name = "Authentication",
Description = "Authentication Basics",
URL = "https://asp-net-core.com/auth"
},
new Link()
{
Name = "Routing",
Description = "Configure routing for web apps",
URL = "https://asp-net-core.com/routing"
},
new Link()
{
Name = "API",
Description = "Create a REST API with ASP.NET Core",
URL = "https://asp-net-core.com/API"
}
}
}
};
context.Companies.AddRange(newCompanies);
context.SaveChanges();
So, is it possible to eager load the Links for a company with Include()? Yes, nothing is different.
var companies = context.Companies
.Include(c => c.Links)
.ToList();
foreach (var company in companies)
{
foreach (var link in company.Links)
{
Console.WriteLine($"{link.CompanyName} --> {link.Name} at {link.URL}");
}
}
And the output:
EF Core Enterprises --> Introduction at https://efcore.com/intro
EF Core Enterprises --> Mapping at https://efcore.com/mapping
EF Core Enterprises --> Change Tracker at https://efcore.com/changetracker
ASP.NET Core Inc. --> Authentication at https://asp-net-core.com/auth
ASP.NET Core Inc. --> Routing at https://asp-net-core.com/routing
ASP.NET Core Inc. --> API at https://asp-net-core.com/API
Sorry for being too lengthy, I just like to explain everything as clearly as possible. Here's the documentation concerning relationships. I hope you find this useful.

Related

Retrieve child entities from CrudAppService in abp.io using .Net 5 EF

I'm using the latest version of ABP from abp.io and have two entities with a many-many relationship. These are:
public class GroupDto : AuditedEntityDto<Guid>
{
public GroupDto()
{
this.Students = new HashSet<Students.StudentDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Students.StudentDto> Students { get; set; }
}
and
public class StudentDto : AuditedEntityDto<Guid>
{
public StudentDto()
{
this.Groups = new HashSet<Groups.GroupDto>();
}
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual ICollection<Groups.GroupDto> Groups { get; set; }
}
I set up the following test to check that I am retrieving the related entities, and unfortunately the Students property is always empty.
public async Task Should_Get_List_Of_Groups()
{
//Act
var result = await _groupAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(g => g.Name == "13Ck" && g.Students.Any(s => s.Name == "Michael Studentman"));
}
The same is true of the equivalent test for a List of Students, the Groups property is always empty.
I found one single related answer for abp.io (which is not the same as ABP, it's a newer/different framework) https://stackoverflow.com/a/62913782/7801941 but unfortunately when I add an equivalent to my StudentAppService I get the error -
CS1061 'IRepository<Student, Guid>' does not contain a definition for
'Include' and no accessible extension method 'Include' accepting a
first argument of type 'IRepository<Student, Guid>' could be found
(are you missing a using directive or an assembly reference?)
The code for this is below, and the error is being thrown on the line that begins .Include
public class StudentAppService :
CrudAppService<
Student, //The Student entity
StudentDto, //Used to show students
Guid, //Primary key of the student entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateStudentDto>, //Used to create/update a student
IStudentAppService //implement the IStudentAppService
{
private readonly IRepository<Students.Student, Guid> _studentRepository;
public StudentAppService(IRepository<Student, Guid> repository)
: base(repository)
{
_studentRepository = repository;
}
protected override IQueryable<Student> CreateFilteredQuery(PagedAndSortedResultRequestDto input)
{
return _studentRepository
.Include(s => s.Groups);
}
}
This implements this interface
public interface IStudentAppService :
ICrudAppService< // Defines CRUD methods
StudentDto, // Used to show students
Guid, // Primary key of the student entity
PagedAndSortedResultRequestDto, // Used for paging/sorting
CreateUpdateStudentDto> // Used to create/update a student
{
//
}
Can anyone shed any light on how I should be accessing the related entities using the AppServices?
Edit: Thank you to those who have responded. To clarify, I am looking for a solution/explanation for how to access entities that have a many-many relationship using the AppService, not the repository.
To aid with this, I have uploaded a zip file of my whole source code, along with many of the changes I've tried in order to get this to work, here.
You can lazy load, eagerly load or configure default behaviour for the entity for sub-collections.
Default configuration:
Configure<AbpEntityOptions>(options =>
{
options.Entity<Student>(studentOptions =>
{
studentOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Groups);
});
});
Eager Load:
//Get a IQueryable<T> by including sub collections
var queryable = await _studentRepository.WithDetailsAsync(x => x.Groups);
//Apply additional LINQ extension methods
var query = queryable.Where(x => x.Id == id);
//Execute the query and get the result
var student = await AsyncExecuter.FirstOrDefaultAsync(query);
Or Lazy Load:
var student = await _studentRepository.GetAsync(id, includeDetails: false);
//student.Groups is empty on this stage
await _studentRepository.EnsureCollectionLoadedAsync(student, x => x.Groups);
//student.Groups is filled now
You can check docs for more information.
Edit:
You may have forgotten to add default repositories like:
services.AddAbpDbContext<MyDbContext>(options =>
{
options.AddDefaultRepositories();
});
Though I would like to suggest you to use custom repositories like
IStudentRepository:IRepository<Student,Guid>
So that you can scale your repository much better.

ReferenceLoopHandling.Ignore not working for Azure Mobile App Service to ignore serialization of circular references

I'm using the following code in my startup class to prevent errors serializing my entities which may cause circular references, but it is not working.
Why?
public partial class Startup
{
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
config.MapHttpAttributeRoutes();
// Use Entity Framework Code First to create database tables based on your DbContext
Database.SetInitializer(new MobileServiceInitializer());
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
config.Formatters.JsonFormatter.SerializerSettings.Re‌​ferenceLoopHandling = ReferenceLoopHandling.Ignore;
config.Services.Add(typeof(IExceptionLogger), new AiExceptionLogger());
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
app.UseWebApi(config);
}
}
According to your description, I created my Azure Mobile App project to test this issue. Based on your Startup.cs, I added my apiController as follows:
[MobileAppController]
public class ValuesController : ApiController
{
[Route("api/values")]
public HttpResponseMessage Get()
{
Department sales = new Department() { Name = "Sales" };
Employee alice = new Employee() { Name = "Alice", Department = sales };
sales.Manager = alice;
return Request.CreateResponse(sales);
}
}
public class Employee
{
public string Name { get; set; }
//[JsonIgnore]
public Department Department { get; set; }
}
public class Department
{
public string Name { get; set; }
public Employee Manager { get; set; }
}
When access this endpoint, I encountered the following XML Circular Object References error:
Note: For a simple way, I removed the XML Formatter via config.Formatters.Remove(config.Formatters.XmlFormatter);. Also, you could refer to the section about preserving object references in XML from Handling Circular Object References.
After I removed XML Formatter, then I encountered the following error about object references loop in JSON:
Then, I followed this Loop Reference handling in Web API code sample, but without luck in the end. Also, I tried to create a new Web API project and found that ReferenceLoopHandling.Ignore could work as expected.
In the end, I found that if I remove the MobileAppController attribute from my
apiController, then it could work as follows:
In summary, I assumed that you could try to ignore the reference attributes with the JsonIgnore for JSON.NET, for more details you could refer to fix 3:Ignore and preserve reference attributes.

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();
}
}

Entity Framework: relate identity 2 user to product model

I'm using Web API 2 and Entity Framework 6 and Identity 2
I have product model which relates to an ApplicationUser model, where I create Product, I get an error:
Additional information: An entity object cannot be referenced by multiple instances of IEntityChangeTracker.
My model:
public class Product {
public int id { get; set; }
public string url { get; set; }
public string name {get;set}
public ApplicationUser user { get; set; }
}
My create code:
public IHttpActionResult PostProduct(Product product) {
ApplicationUserManager userManager = new ApplicationUserManager(new UserStore<ApplicationUser>(new ApplicationDbContext()));
product.user = userManager.FindById(User.Identity.GetUserId());
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
db.Products.Add(product);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = product.id }, product);
}
What dbcontext does "db" refer to?
I would guess that you probably have more than one dbcontext and you are trying to retrieve data from one and persist it on another.
Maybe you can change the code to create just one dbcontext per http request and reuse that context during your http post request.
Maybe change:
ApplicationUserManager userManager = new ApplicationUserManager(new UserStore<ApplicationUser>(new ApplicationDbContext()));
to:
ApplicationUserManager userManager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
This will let you save your product but you should avoid sending back an EF object, or at least switch off lazy loading.
To clarify a bit, don't send product back in the return as it contains properties which are EF proxies for lazy loading. This will cause at best, a lot more data than you want going back when you serialise the product object and at worst - the error you describe in the comments.

How to change the naming convention of Many-to-Many table relationships?

How can I go about changing the naming convention of the auto-generated many-to-many table?
Assume I have two classes:
public class User
{
public int UserId { get; set; }
public virtual List<Role> Roles { get; set; }
}
public class Role
{
public int RoleId { get; set; }
public virtual List<User> Users { get; set; }
}
By Default, this will create a table called UserRoles.
I can change the name of that one table to UsersInRoles, for example, by using the following in the OnModelCreating override of my DbContext:
modelBuilder.Entity<User>()
.HasMany(p => p.Roles)
.WithMany(p => p.Users)
.Map(mc =>
{
mc.MapLeftKey("UserId");
mc.MapRightKey("RoleId");
mc.ToTable("UsersInRoles");
});
However, what I really want to do is change the naming convention so that by default, all auto-generated many-to-many tables use this new convention. I cannot figure out how to do that, or if it's even possible. I do not like having to specify 9 lines of extra code every time I specify one of these relationships.
I am currently using EF version 6.0.0-rc1.
The ability to control relationships was removed from the basic conventions API before release because it wasn't in a usable state. You can access all of the properties and tables in the model through model based conventions. An overview of model based conventions is available here: http://msdn.microsoft.com/en-US/data/dn469439
This solution involves a little more digging around in the metadata API, EntitySet is the correct type for this scenario
This convention should rename the generated relation table:
public class MyConvention : IStoreModelConvention<EntitySet>
{
public void Apply(EntitySet set, DbModel model)
{
var properties = set.ElementType.Properties;
if (properties.Count == 2)
{
var relationEnds = new List<string>();
int i = 0;
foreach (var metadataProperty in properties)
{
if (metadataProperty.Name.EndsWith("_ID"))
{
var name = metadataProperty.Name;
relationEnds.Add(name.Substring(0, name.Length - 3));
i++;
}
}
if (relationEnds.Count == 2)
{
set.Table = relationEnds.ElementAt(0) + "_" + relationEnds.ElementAt(1) + "_RelationTable";
}
}
}