I am looking for some guidance with Web API functionality that was a breeze in MVC in regards to updating entities that have navigational properties.
In MVC, it was accomplished as:
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public virtual async Task<ActionResult> Update(Page page)
{
Guard.IsNotNull(page, "page");
var pageToUpdate = await this.repository.Query.Include(p => p.Tags).Include(p => p.Name).SingleOrDefaultAsync(p => p.Pk == page.Pk);
if (pageToUpdate == null)
{
return this.RedirectToRoute(H.Constants.Routes.Error.Index, new
{
view = H.Constants.Views.Error.ViewPages.NotFound
});
}
if (this.TryUpdateModel(pageToUpdate))
{
this.repository.BeginTransaction();
this.repository.Update(pageToUpdate); // Updates related entities!
await this.repository.CommitTransactionAsync();
return this.RedirectToRoute(H.Constants.Routes.Data.Read);
}
return this.View(H.Constants.Views.FolderNames.ViewPages.FormatWith(H.Constants.Views.Data.ViewPages.Update), pageToUpdate);
}
All navigational properties would be updated, and life was well.
When attempting this exact thing in Web API, not so much. The related entities are not updated. Example:
[HttpPatch]
[HttpPut]
public virtual async Task<IHttpActionResult> Update(int pk, Page page)
{
Guard.IsNotNegativeOrZero(pk, "pk");
if (this.ModelState.IsValid)
{
if (page.Pk == pk)
{
try
{
this.repository.BeginTransaction();
this.repository.Update(page); // Doesn't update related entities.
await this.repository.CommitTransactionAsync();
return this.StatusCode(HttpStatusCode.NoContent);
}
catch (DbUpdateConcurrencyException dbUpdateConcurrencyException)
{
if (this.repository.Query.Any(p => p.Pk == pk))
{
return this.InternalServerError(dbUpdateConcurrencyException);
}
return this.NotFound();
}
}
return this.BadRequest();
}
return this.BadRequest(this.ModelState);
}
Is this possible to do today in Web API? Or is Web API currently an incomplete Microsoft product?
Edit: Updated with example and not referencing WCF
TryUpdateModel doesn't exist in WebAPI. You may also find you have issues with seralization of entities. For both those reasons I use AutoMapper to map ViewModels onto EF entities and you can use Mappings in automapper to deal with your navigation properites.
The web API method looks something like this:
public HttpResponseMessage Post(MyViewModel model)
{
if (someValidationCheckHere)
return new HttpResponseMessage(HttpStatusCode.BadRequest);
_myService.Update(Mapper.Map<MyViewModel, MyEntity>(model));
return new HttpResponseMessage(HttpStatusCode.OK);
}
As others have said the service and ultimately the repository does the update. In this example I'm ignoring the navigation property using automapper, but you can configure automapper to handle them how you like:
Mapper.CreateMap<MyViewModel, MyEntity>()
.ForMember(x => x.NavigationProperty, opt => opt.Ignore());
I set up all my mappings in a static class called on application_start in global.asax.
Related
I'm currently exploring the GraphQL development and I'm currently exploring what kind of SQL queries are Generated via EF Core and I observed that no matter that my GraphQL query includes only a few fields the EF Core sends SQL Select for all fields of the Entity.
This is the code I'm using now:
public class DoctorType : ObjectGraphType<Doctors>
{
public DoctorType()
{
Field(d => d.PrefixTitle);
Field(d => d.FName);
Field(d => d.MName);
Field(d => d.LName);
Field(d => d.SufixTitle);
Field(d => d.Image);
Field(d => d.EGN);
Field(d => d.Description);
Field(d => d.UID_Code);
}
}
public class Doctors : ApplicationUser
{
public string Image { get; set; }
[StringLength(50)]
public string UID_Code { get; set; }
}
the query I'm using is
{
doctors{
fName
lName
}
}
The SQL generated selects all fields of the Doctor entity.
Is there any way to further optimize that the generated SQL query from EF Core?
I'm guessing this happens because the DoctorType inherits from ObjectGraphType<Doctors> and not from some Projection of the Doctor, but I can't think of a clever workaround of this?
Any suggestions?
EDIT:
I'm using GraphQL.NET (graphql-dotnet) by Joe McBride version 2.4.0
EDIT 2:
Either I'm doing it wrong or I don't know.
As one of the comments suggested i downloaded GraphQL.EntityFramework Nuget package by SimonCropp
I did all the configuration needed for it:
services.AddDbContext<ScheduleDbContext>(options =>
{
options.UseMySql(Configuration.GetConnectionString("DefaultConnection"));
});
using (var myDataContext = new ScheduleDbContext())
{
EfGraphQLConventions.RegisterInContainer(services, myDataContext);
}
My Object graph Type is looking as follows
public class SpecializationType : EfObjectGraphType<Specializations>
{
public SpecializationType(IEfGraphQLService graphQlService)
:base(graphQlService)
{
Field(p => p.SpecializationId);
Field(p => p.Code);
Field(p => p.SpecializationName);
}
}
My query looks is:
public class RootQuery : EfObjectGraphType
{
public RootQuery(IEfGraphQLService efGraphQlService,
ScheduleDbContext dbContext) : base(efGraphQlService)
{
Name = "Query";
AddQueryField<SpecializationType, Specializations>("specializationsQueryable", resolve: ctx => dbContext.Specializations);
}
}
and I'm using this graphQL query
{
specializationsQueryable
{
specializationName
}
}
The debug log show that the generated SQL query is
SELECT `s`.`SpecializationId`, `s`.`Code`, `s`.`SpecializationName`
FROM `Specializations` AS `s`
even though I want only specializationName field and I'm expecting it to be:
SELECT `s`.`SpecializationName`
FROM `Specializations` AS `s`
UPDATE
I guess so far I didn't understand how graphQL really worked. I thought that there is some behind the scene fetch of data but there isn't.
The primary fetch is done in the query's field resolver :
FieldAsync<ListGraphType<DoctorType>>("doctors", resolve: async ctx => await doctorServices.ListAsync());
and as long the result to the resolver is the full object in my case the resolver return List of Doctors entity, it will query the Database for the whole entity (all fields). No optimisations are done out of the box from GraphQL doesn't matter if you return IQueryable or else of the entity you are querying.
Every conclusion here is thought of mine it is not 100% guaranteed right
So what I've did is create a group of Helper methods which are creating an selection Expression to use in the LINQ query. The helpers are using resolver's context.SubFields property to get the fields needed.
The problem is that you need for every level of the query only the leaves, say some query "specializations" with "SpecializationName" and "Code" and the "Doctors" with their "Name" and else. In this case in the RootQuery specializations field's resolver you need only the Specializations entity projection so: SpecializationName and Code , then when it goes to fetch all Doctors from the "doctors" Field in SpecializationType the resolver's context has different SubFields which should be used for the projection of the Doctor.
The problem with the above is, when you use query batches i guess even if you dont the thing is that the Doctors Field in SpecializationType needs the SpecializationId fetched in the RootQuery specializations Field.
I guess i didn't explain good what i went through.
Base line is as far as I understand we have to dynamically create selectors which the linq should use to project the entity.
I'm posting my approach here:
public class RootQuery : EfObjectGraphType
{
public RootQuery(IEfGraphQLService efGraphQlService, ISpecializationGraphQlServices specializationServices,
IDoctorGraphQlServices doctorServices, ScheduleDbContext dbContext) : base(efGraphQlService)
{
Name = "Query";
FieldAsync<ListGraphType<SpecializationType>>("specializations"
, resolve: async ctx => {
var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
var expression = BuildLinqSelectorObject.DynamicSelectGenerator<Specializations>(selectedFields.ToArray());
return await specializationServices.ListAsync(selector: expression);
});
}
}
SpecializationType
public class SpecializationType : EfObjectGraphType<Specializations>
{
public SpecializationType(IEfGraphQLService graphQlService
, IDataLoaderContextAccessor accessor, IDoctorGraphQlServices doctorServices)
: base(graphQlService)
{
Field(p => p.SpecializationId);
Field(p => p.Code);
Field(p => p.SpecializationName);
Field<ListGraphType<DoctorType>, IEnumerable<Doctors>>()
.Name("doctors")
.ResolveAsync(ctx =>
{
var selectedFields = GraphQLResolverContextHelpers.GetFirstLevelLeavesNamesPascalCase(ctx.SubFields);
selectedFields = GraphQLResolverContextHelpers.AppendParrentNodeToEachItem(selectedFields, parentNode: "Doctor");
selectedFields = selectedFields.Union(new[] { "Specializations_SpecializationId" });
var expression = BuildLinqSelectorObject.BuildSelector<SpecializationsDoctors, SpecializationsDoctors>(selectedFields);
var doctorsLoader = accessor.Context
.GetOrAddCollectionBatchLoader<int, Doctors>(
"GetDoctorsBySpecializationId"
, (collection, token) =>
{
return doctorServices.GetDoctorsBySpecializationIdAsync(collection, token, expression);
});
return doctorsLoader.LoadAsync(ctx.Source.SpecializationId);
});
}
}
DoctorsServices:
public class DoctorGraphQlServices : IDoctorGraphQlServices
{
public ScheduleDbContext _dbContext { get; set; }
public DoctorGraphQlServices(ScheduleDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Doctors>> ListAsync(int? specializationId = null)
{
var doctors = _dbContext.Doctors.AsQueryable();
if(specializationId != null)
{
doctors = doctors.Where(d => d.Specializations.Any(s => s.Specializations_SpecializationId == specializationId));
}
return await doctors.ToListAsync();
}
public async Task<ILookup<int, Doctors>> GetDoctorsBySpecializationIdAsync(IEnumerable<int> specializationIds, CancellationToken token, Expression<Func<SpecializationsDoctors, SpecializationsDoctors>> selector = null)
{
var doctors = await _dbContext.SpecializationsDoctors
.Include(s => s.Doctor)
.Where(spDocs => specializationIds.Any(sp => sp == spDocs.Specializations_SpecializationId))
.Select(selector: selector)
.ToListAsync();
return doctors.ToLookup(i => i.Specializations_SpecializationId, i => i.Doctor);
}
}
SpecializationServices
public class SpeciaizationGraphQlServices : ISpecializationGraphQlServices
{
public ScheduleDbContext _dbContext { get; set; }
public SpeciaizationGraphQlServices(ScheduleDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<dynamic> ListAsync(string doctorId = null, Expression<Func<Specializations, Specializations>> selector = null)
{
var specializations = _dbContext.Specializations.AsQueryable();
if (!string.IsNullOrEmpty(doctorId))
{
specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
}
return await specializations.Select(selector).ToListAsync();
}
public async Task<ILookup<string, Specializations>> GetSpecializationsByDoctorIdAsync(IEnumerable<string> doctorIds, CancellationToken token)
{
var specializations = await _dbContext.SpecializationsDoctors
.Include(s => s.Specialization)
.Where(spDocs => doctorIds.Any(sp => sp == spDocs.Doctors_Id))
.ToListAsync();
return specializations.ToLookup(i => i.Doctors_Id, i => i.Specialization);
}
public IQueryable<Specializations> List(string doctorId = null)
{
var specializations = _dbContext.Specializations.AsQueryable();
if (!string.IsNullOrEmpty(doctorId))
{
specializations = specializations.Where(s => s.Doctors.Any(d => d.Doctors_Id == doctorId));
}
return specializations;
}
}
This post has become pretty large, sorry for the span..
For DoctorType, check the defined ObjectGraphType which is used to return Doctors.
For example, I have PlayerType like below:
public class PlayerType : ObjectGraphType<Player>
{
public PlayerType(ISkaterStatisticRepository skaterStatisticRepository)
{
Field(x => x.Id);
Field(x => x.Name, true);
Field(x => x.BirthPlace);
Field(x => x.Height);
Field(x => x.WeightLbs);
Field<StringGraphType>("birthDate", resolve: context => context.Source.BirthDate.ToShortDateString());
Field<ListGraphType<SkaterStatisticType>>("skaterSeasonStats",
arguments: new QueryArguments(new QueryArgument<IntGraphType> { Name = "id" }),
resolve: context => skaterStatisticRepository.Get(context.Source.Id), description: "Player's skater stats");
}
}
And I return Field<ListGraphType<PlayerType>> by
public class NHLStatsQuery : ObjectGraphType
{
public NHLStatsQuery(IPlayerRepository playerRepository, NHLStatsContext dbContext)
{
Field<ListGraphType<PlayerType>>(
"players",
resolve: context => {
return dbContext.Players.Select(p =>new Player { Id = p.Id, Name = p.Name });
//return playerRepository.All();
});
}
}
For the query and its columns, it is controlled by resolve in Field.
No matter what fields you want to return, make sure the columns defined in PlayerType are returned in resolve.
I suggest you:
1-use dto models and map them with database models
This means that you need to convert input dto model in database model to save in db; and also convert database models got from entity framework database select into dto model.
This is the classic approach used when you made a generic api, that for example get dto model data in input request, convert dto to save data in database, and viceversa.
2-map dto model to graphqltypes (objectgraphtype and inputobjectgraphtype)
This means that for each dto model could be necessary write 1 objectgraphtype and 1 inputobjectgraphtype.
TO DO THIS I'VE CREATE AN AUTOMATIC DTO TO GRAPHTYPE CONVERTER, so you don't need to write K and K of codes!! (see link at the end)
3-DON'T USE ADDDBCONTEXT! Graphql middleware use a singleton pattern; everything used via Dependecy injection in graphql is singleton externally, even if it is register as scoped (AddDbContext means "scoped").
This means that you have 1 connection opened to startup. You can't do 2 db operation in the same time!
In the real life you can't use AddDbContext with Graphql!
You can use factory pattern to do this. So, don't pass dbcontext in Dependency injection, but a Func and instantiate dbcontext explicitally.
Here a complete implementation example:
https://github.com/graphql-dotnet/graphql-dotnet/issues/576#issuecomment-626661695
There was a talk about GraphQL with EF Core 6 by #jeremylikness on .NET Conf 2021. I would recommend using .NET 6 and check his talk out:
https://devblogs.microsoft.com/dotnet/get-to-know-ef-core-6/#graphql
https://aka.ms/graphql-efcore
https://www.youtube.com/watch?v=GBvTRcV4PVA
https://www.youtube.com/watch?v=4nqjB_z5CU0
Here is an example implementation using Hot Chocolate GraphQL server:
https://chillicream.com/docs/hotchocolate/integrations/entity-framework
This is what Microsoft wrote about GraphQL for EF Core 6.0 in their High-level plan:
GraphQL has been gaining traction over the last few years across
a variety of platforms. We plan to investigate the space and find ways
to improve the experience with .NET. This will involve working with
the community on understanding and supporting the existing ecosystem.
It may also involve specific investment from Microsoft, either in the
form of contributions to existing work or in developing complimentary
pieces in the Microsoft stack.
https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/plan#graphql
To enable automatic fields projection, having your DB context set up as usual for .NET 6, add hotchocolate server:
dotnet add package HotChocolate.Data.EntityFramework
Expose some data to it:
public class MyQueries
{
[UseProjection] // Enables field projection
public IQueryable<Book> Books([Service] MyContext db) => db.Books;
}
Enable it in Program.cs:
builder.Services.AddGraphQLServer().AddQueryType<MyQueries>().AddProjections();
...
app.MapGraphQL("/graphql");
That should be enough to ensure automatic db fields projection. Now you can run GraphQL queries via builder generated at /graphql/, while monitoring SQL via MyContext.Database.Log = Console.Write;
I'm using GraphQL.NET (graphql-dotnet) by Joe McBride version 2.4.0
First of all, I'd recommend updating at least to v4.6 - there are lots of fixes and useful updates.
Secondly, if you don't have data mutation (means - update/delete/insert data), I'd say that better to not use EF for fetching data. Based on the same GraphQL.Net lib you can take a look, for example, NReco.GraphQL is using lightweight-ORM to fetch and map data (you just need to define a schema in json-file).
I'm on an assignment to to expose SQL data using MVC via OData.
I'm working with an existing project, Visual Studio 2015.
A bunch of tables have already been exposed.
Please first accept my apologies for perhaps a poorly crafted post.
I'm having a hard time figuring out what I'm actually working with.
In addition, I've had only a day to familiarize myself with this project.
I know that I said MVC but as far as I can tell, this project does not have VIEWS. I do believe however that the consumers of this project will read JSON.
I've used the Entity Framework to build the MODEL for the additional tables required to finish my assignment.
I'm working now on the CONTROLLER code, and I'd like to use a tool to automate that portion as much as possible. Below please find an example of a CONTROLLER already defined. I include that to help you get a feel for the type of tool I'm looking for.
Does such a tool exist? Or do I have to notepad a CONTROLLER for the tables that I've added to the project?
Thank you kindly for reading my post and for any assistance you can offer :)
public class BlockController : ODataController
{
AccordNewModel _db = new AccordNewModel();
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IHttpActionResult Get()
{
return Ok(_db.Block.AsQueryable());
}
[ODataRoute()]
[HttpPost]
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IHttpActionResult Post(Block newBlock)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_db.Block.Add(newBlock);
_db.SaveChanges();
return Created(newBlock);
}
[ODataRoute()]
[HttpPut]
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IHttpActionResult Put(Block block)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_db.Block.AddOrUpdate(p => new { p.BlockID }, block);
_db.SaveChanges();
return Updated(block);
}
[HttpDelete]
public IHttpActionResult Delete([FromODataUri] int key)
{
var block = _db.Block.SingleOrDefault(t => t.BlockID == key);
_db.Block.Remove(block);
_db.SaveChanges();
return Content(HttpStatusCode.NoContent, "Deleted");
}
protected override void Dispose(bool disposing)
{
_db.Dispose();
base.Dispose(disposing);
}
}
It makes little sense to write very similar controller code multiple times, I would advise against generating 1 controller per entity. Instead you could use a generic solution:
public class BaseController<T> : ODataController
{
AccordNewModel _db = new AccordNewModel();
[EnableQuery]
public IHttpActionResult Get()
{
return Ok(_db.Set<T>().AsQueryable());
}
[HttpPost]
public IHttpActionResult Post(T posted)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var added = _db.Set<T>().Add(posted);
_db.SaveChanges();
return Created(added);
}
//Etc... Write generic controller methods using Db.Set<T>()
Then for every entity you need not do much, here is how your 'block' entity controller would look:
public class BlockController : BaseController<Block> { }
For delete and update you need some way to identify the generic object of T by Id (int key). I know of two ways to do this:
1: Let your entities implement an interface IHasId which ensures they have an int Id property, then add a generic constraint to the BaseController class like so: public class BaseController<T> : ODataController where T : IHasId. Delete method could look like this:
[HttpDelete]
public IHttpActionResult Delete([FromODataUri] int key)
{
var found = _db.Set<T>().FirstOrDefault(e => e.Id == key);
if(found != null)
{
_db.Set<T>().Remove(found);
_db.SaveChanges();
return StatusCode(System.Net.HttpStatusCode.NoContent);
}
else
{
return NotFound();
}
}
Or, 2: Make the BaseController class abstract and add: protected abstract T GetById(int id);. Then inheriting classes (such as 'BlockController') must implement a method to get the object from the Db by id. You will have to implement this method for every entity, this is still less work than writing individual controllers for every entity. The delete method would look almost the same as the one above except: var found = GetById(key);.
I use Delete as an example, but if you have some way to get an entity by id you can quite easily implement the Post and possibly a Get(int key) as well.
With this generic base class the code per entity is minimal and writing it for every entity shouldn't be too much work.
I'm using AutoMapper for mapping between my DTOs and business objects. This works fine.
However, I have a Web API which accepts PUT requests in order to update entities. The action looks like this:
public virtual async Task<IHttpActionResult> Put(string id, [FromBody] ProjectDto projectDto)
When mapping from ProjectDto to Project (business object), it maps all values from ProjectDto. Let's say I want to update a project's name, by sending the following JSON to the API:
{
"projectId": 10,
"name": "The new name"
}
Since I haven't given a value for all of the other properties of ProjectDto, AutoMapper will map default values where I haven't supplied a value.
For example, if ProjectDto had a couple of extra fields, they would now be null:
{
"projectId": 10,
"name": "The new name",
"createdDate": null,
"manager": null
}
..etc.
My mapping looks like this:
cfg.CreateMap<ProjecDto, Project>()
.ForAllMembers(opt => opt.Condition(x => x != null));
.. but AutoMapper is still mapping null values.
I have looked at this question, but I just get "ProjectDto does not contain a definition for IsSourceValueNull" when I try to copy their solution.
The Web API controller action (it's generic, TDto is an empty marker interface. T and TDto resolves at runtime to Project and ProjectDto, respectively.):
public virtual async Task<IHttpActionResult> Put(string id, [FromBody] TDto model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var entity = _dataContext.GetRepository<T>().GetById(id);
if (entity == null)
return NotFound();
var entityToUpdate = _mapper.Map<T>(model);
var updatedEntity = _dataContext.GetRepository<T>().Update(entity);
var updatedEntityDto = _mapper.Map<TDto>(updatedEntity);
return Ok(updatedEntityDto);
}
Fundamentally this is how it should be, You need to put full object with changed values. PUT expect full object as per REST standards (api/project) - and not partial or dirty object.
if you want to update particular property then use different end point for ex.
api/project/prop1
api/project/prop2
and then put relevant values to them.
If you want to override this behaviour then write your own mappings to put those conditions.
please use null substitution feature in auto-mapper..
https://github.com/AutoMapper/AutoMapper/wiki/Null-substitution
option: 2 (may be Hack)
cfg.CreateMap<ProjecDto, Project>();
cfg.CreateMap<Project, Project>()
.ForAllMembers(opt => opt.Condition(x => x != null));
and API
public virtual async Task<IHttpActionResult> Put(string id, [FromBody] TDto model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var entity = _dataContext.GetRepository<T>().GetById(id);
if (entity == null)
return NotFound();
var entityToUpdate = _mapper.Map<T>(model);
entityToUpdate = _mapper.Map<T, T>(entityToUpdate, entity); // overwriting the db object with new vals, may be swap around it, I haven't tested.
var updatedEntity = _dataContext.GetRepository<T>().Update(entityToUpdate);
var updatedEntityDto = _mapper.Map<TDto>(updatedEntity);
return Ok(updatedEntityDto);
}
It looks like Entity Framework 7, while lazy loading isn't supported / implemented, is returning all relationships for a given entity without using the Include(...) method.
I'm not sure I follow what the expected behavior should be in this case. See my reproduction below. Note, this github branch/commit reproduces this.
Add DbContext and a repository provider:
// In ConfigureServices
services.AddDbContext<PouarfDbContext>(options => options.UseInMemoryDatabase());
services.AddScoped<IContactProvider, ContactProvider>();
Seed application data:
// In Configure
// CreateSampleData() adds a bunch of `People` objects to a list,
// and then loops through them, adding each one to the DbContext
// through the `ContactProvider`
Task.Run(async () =>
await new
MockPeople(app
.ApplicationServices
.GetService<IContactProvider>()).CreateSampleData());
Couple of examples from ContactProvider implementation:
public async Task AddPerson(Person person)
{
await Task.Run(() => _dbContext.Add(person));
}
public async Task<IEnumerable<Person>> GetPeople()
{
// Notice no Include(...)
return await _dbContext.People.ToListAsync();
}
Person object:
public class Person : ContactInformationBase // Contains Id
{
...
public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
}
And then in my controller action, I'm getting the people from EF:
public async Task<IActionResult> Index()
{
// Contains a populated list of `PhoneNumbers`, even though
// they should only be included if I explicitly asked EF7 to load them.
var people = await _contactProvider.GetPeople();
..
}
So basically, EF7 is providing mapped entities from my main object, even though they should only be included when I explicitly ask for them, via an Include(p => p.PhoneNumbers).
I'm using ASP.NET Core 1 RC2, with Entity Framework 7
EDIT
Just added an API action to my controller, and the results are... odd.
[Route("api/[action]")]
public async Task<IEnumerable<Person>> People()
{
// True adds all the Includes off the DbSet as needed.
return await _contactProvider.GetPeople(true);
}
Now I'm not getting back any mapped entities with this call. If I remove or add Include() call, it doesn't have an effect on the result. Only the core Person entity is returned, all mapped entities are null.
As I noted here, the issue was my own fault.
The issue was in the approach of utilizing the Include pattern on the DbSet.
I had this, WRONG:
var people = _dbContext.People;
if (includeRelationships)
{
people.Include(p => p.PhoneNumbers)
.Include(p => p.EmailAddresses)
.Include(p => p.StreetAddresses);
}
return await people.FirstOrDefaultAsync(p => p.Id.Equals(id));
And changed it to this, notice the actual applying of the Include here, RIGHT:
IQueryable<Person> people = _dbContext.People;
if (includeRelationships)
{
people = people
.Include(p => p.EmailAddresses)
.Include(p => p.PhoneNumbers)
.Include(p => p.StreetAddresses);
}
return await people.FirstOrDefaultAsync(p => p.Id.Equals(id));
I've been tasked with the glorious mission of modernizing a CRUD system (Ironic). I'm using EF 6.1.1 and WebApi. I send the Entity using a DTO (dynamic object for now will create a static type when all is working, will also make the calls async)
[HttpGet]
public dynamic Get(int id)
{
var p = personRepository.Get(id);
return new
{
p.PersonId,
p.BirthYear,
p.DeathYear,
p.Comment,
PersonNames = p.PersonNames.Select(pn => new { pn.PersonId, pn.PersonNameId, pn.NameTypeId, pn.Firstname, pn.Prefix, pn.Surname })
};
}
I then update fields on the Person object or add/remove/update PersonNames collection using Knockout and ko.mapping
I post the result to this WebApi method
[HttpPost]
public void Post([FromBody]Person person)
{
personRepository.Merge(person);
}
All the id's etc are correct, in personRepository.Merge i've tried
public void Merge(Person person)
{
db.People.AddOrUpdate(person);
db.SaveChanges();
}
This works for the fields directly on the personobject, but it does not work with add/remove/update on the PersonNames
Can I support this without writing manual merge code?
Solution
Ended up doing this
public void Merge(Person person)
{
db.People.AddOrUpdate(person);
var existingNames = db.PersonNames.Where(pn => pn.PersonId == person.PersonId).ToList();
var deleted = existingNames.Where(pn => person.PersonNames.All(d => d.PersonNameId != pn.PersonNameId));
db.PersonNames.RemoveRange(deleted);
foreach (var name in person.PersonNames)
{
db.PersonNames.AddOrUpdate(name);
}
db.SaveChanges();
}
Given you've mentioned this is a CRUD system this all looks sensible to me. I can't see how you can really avoid mapping from your DTO's back to your domain entities (e.g. Person)
I expect you've thought of it already, but how about removing as much boiler plate merge code as possible using something like AutoMapper? https://github.com/AutoMapper/AutoMapper
Mapping does get complicated for navigational properties (i.e. mapping between an object graph of DTO's back to an object graph of entities). This link goes into far more detail than I can: http://rogeralsing.com/2013/12/01/why-mapping-dtos-to-entities-using-automapper-and-entityframework-is-horrible/