Web API, EF Code First and Duplicate Records - ef-code-first

I am creating a REST service using Web API and am using EF Code First to retrieve and store data to back end that service. I have a bunch of classes defined and the EF creates the database no problems at all. I can retrieve the data and again have no issues. The problem I experience is when I try to write back a record. If I have created the following little mock up to reproduce the problem.
public class Company
{
[Key]
public int CompanyID { get; set; }
public string Name { get; set; }
}
public class Contact
{
[Key]
public int ContactID { get; set; }
public int CompanyID { get; set; }
public virtual Company Company { get; set; }
public string Name { get; set; }
}
public class Quote
{
[Key]
public int QuoteID { get; set; }
public int CustomerID { get; set; }
[ForeignKey("CustomerID")]
public virtual Company Customer { get; set; }
public int ContactID { get; set; }
public virtual Contact Contact { get; set; }
public string Reference { get; set; }
}
I then have three controllers in my REST service;
public class CompaniesController : ApiController
{
TestDBContext Context = new TestDBContext();
public IEnumerable<Company> GetCompanies()
{
return Context.Companies;
}
}
[Route("api/companies/{CompanyID}/contacts")]
public class CompanyContactsController : ApiController
{
TestDBContext Context = new TestDBContext();
public IEnumerable<Contact> GetCompanyContacts(int CompanyID)
{
return Context.Contacts.Where(C => C.CompanyID == CompanyID);
}
}
public class QuotesController : ApiController
{
TestDBContext Context = new TestDBContext();
public HttpResponseMessage PostQuote(Quote value)
{
try
{
if (ModelState.IsValid)
{
Context.Quotes.Add(value);
Context.SaveChanges();
// Return the object in the body.
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, value);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = value.QuoteID }));
return response;
}
}
catch
{
}
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
I pre-populate the database with a company and a contact and then have the following code to create a quote.
HttpClient Client = new HttpClient();
string CompanyURL = "http://localhost:56619/api/Companies";
string ContactURL = "http://localhost:56619/api/Companies/{0}/Contacts";
string QuoteURL = "http://localhost:56619/api/Quotes";
Client.GetAsync(CompanyURL).ContinueWith((T) =>
{
T.Result.EnsureSuccessStatusCode();
T.Result.Content.ReadAsAsync<IEnumerable<Company>>().ContinueWith((T2) =>
{
Company Comp = T2.Result.First();
Client.GetAsync(string.Format(ContactURL, Comp.CompanyID)).ContinueWith((T3) =>
{
T3.Result.EnsureSuccessStatusCode();
T3.Result.Content.ReadAsAsync<IEnumerable<Contact>>().ContinueWith((T4) =>
{
Contact Cont = T4.Result.First();
Quote Q = new Quote() { Customer = Comp, Contact = Cont, Reference = "Test" };
JsonMediaTypeFormatter Formatter = new JsonMediaTypeFormatter();
Formatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
Client.PostAsync<Quote>(QuoteURL, Q, Formatter).ContinueWith((T5) =>
{
T5.Result.EnsureSuccessStatusCode();
T5.Result.Content.ReadAsAsync<Quote>().ContinueWith((T6) =>
{
var Res = T6.Result;
});
});
});
});
});
});
I know the code is a little scrappy, but it is just thrown together to illustrate the problem without any extra stuff cluttering the code up.
When the EF saves the changes to the database it creates 1 quote, an extra contact and 2 extra company records. I'm puzzled as to why. I am using VS2013, EF6 and Web API 2.
If anyone has any suggestions they would be very gratefully be received.
Many thanks in advance,
Neil.

Related

Redirecting after deleting an item is always returning null

I'm using ASP.NET MVC to build an application for Forums. I have an entity named Posts and an entity named PostReplies.
On a particular Post, there will be a list of replies which are linked by a FK Post_Id in my PostReplies entity.
When I delete a reply on a post and call:
RedirectToAction(GetPost, Post, new { id = post.id});
(gets the individual post, with list of replies on it)
I get an error relating to this bit of code:
var replies = post.Replies;
(the post, always returns null)
I'm not sure why this is, it always redirects fine when I add a reply and then redirect back to the post.
I feel like I'm doing something fundamentally wrong when I'm calling delete method. I'll expand the logic I have below:
Post entity:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public virtual Discussion Discussion { get; set; }
public virtual ICollection<PostReply> Replies { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
PostReply entity:
public class PostReply
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public virtual Post Post { get; set; }
public virtual ApplicationUser ApplicationUser { get; set; }
}
ReplyController - delete a reply:
[HttpGet]
public ActionResult DeleteReply(int id)
{
return View(_replyService.GetReply(id));
}
[HttpPost]
public ActionResult DeleteReply(int id, PostReply reply, Post posts)
{
var replies = _replyService.GetReply(id);
_replyService.DeleteReply(id, reply, posts);
return RedirectToAction("GetPost", "Post", new { id = posts.Id });
}
_replyService logic (called in the controller above):
public void DeleteReply(int id, PostReply reply, Post posts)
{
var replytoDelete = _context.Replies
.FirstOrDefault(r => r.Id == id);
if (replytoDelete != null)
{
_context.Replies.Remove(replytoDelete);
_context.SaveChanges();
}
}
PostController - get individual post:
public ActionResult GetPost(int id)
{
Post post = _postService.GetPost(id);
var replies = post.Replies;
var listofReplies = replies.Select(reply => new NewPostReplyModel
{
Id = reply.Id,
ReplyPosted = reply.Created,
ReplyContent = reply.Content,
ReplyUserId = reply.ApplicationUser.Id,
ReplyUserName = reply.ApplicationUser.UserName
});
var model = new GetPostViewModel
{
Replies = listofReplies,
Posts = BuildNewPost(post)
};
return View(model);
// return View("GetPost", post);
}
private NewPostModel BuildNewPost(Post post)
{
return new NewPostModel
{
PostId = post.Id,
PostContent = post.Content,
PostTitle = post.Title,
DatePosted = post.Created.ToString(),
DiscussionName = post.Discussion.Title,
DiscussionId = post.Discussion.Id,
UserId = post.ApplicationUser.Id,
UserName = post.ApplicationUser.UserName,
};
}
GetReply logic in service:
public PostReply GetReply(int id)
{
return _context.Replies.Find(id);
}
I think you are not including replies in your GetPost() methode,
So check if your code is like this:
public Post GetPost(int id)
{
return _context.Posts.Include(p=>p.Replies).FirstOrDefault(p=>p.Id == id);
}

EF Core - Many-to-Many relationship doesn't work

I'm having some problems trying to use the many-to-many relationship in EF Core 2.0. Here is me code:
Here are my entities:
User
public class User : IdentityUser
{
private User()
{
}
public String Name { get; private set; }
public ICollection<UserCourse> UserCourses { get; private set; }
public static User Create(string name, string username, string email)
{
var instance = new User
{
Id = Guid.NewGuid().ToString(),
};
instance.Update(name, username, email);
return instance;
}
public void Update(string name, string username, string email)
{
Name = name;
UserName = username;
Email = email;
}
public void Update(UserCreatingModel model)
{
this.UserName = model.Username;
this.Name = model.Name;
this.Email = model.Email;
}
public void Update(UserCourse userCourse)
{
if (UserCourses == null)
{
UserCourses = new List<UserCourse>() {userCourse};
}
else
{
UserCourses.Add(userCourse);
}
}
}
Course entity
public class Course
{
private Course() { }
public Guid Id { get; private set; }
public string Name { get; private set; }
public int Year { get; private set; }
public int Semester { get; private set; }
public List<Lesson> Lessons { get; private set; }
public ICollection<UserCourse> UserCourses { get; private set; }
public static Course Create(string name, int year, int semester, List<Lesson> lessons, List<User> professors)
{
var instance = new Course { Id = Guid.NewGuid() };
instance.Update(name, year, semester, lessons);
return instance;
}
public static Course Create(string name, int year, int semester)
{
var instance = new Course { Id = Guid.NewGuid() };
instance.Update(name, year, semester);
return instance;
}
public void Update(string name, int year, int semester, List<Lesson> lessons)
{
Name = name;
Year = year;
Semester = semester;
Lessons = lessons;
}
public void Update(string name, int year, int semester)
{
Name = name;
Year = year;
Semester = semester;
}
public void Update(UserCourse userCourse)
{
if (UserCourses == null)
{
UserCourses = new List<UserCourse>(){userCourse};
}
else
{
UserCourses.Add(userCourse);
}
}
public void Update(List<Lesson> lessons)
{
this.Lessons = lessons;
}
}
Join entity
public class UserCourse
{
private UserCourse() { }
public string UserId { get; private set; }
public User User { get; private set; }
public Guid CourseId { get; private set; }
public Course Course { get; private set; }
public static UserCourse CreateUserCourse(string userId, User user, Guid coursId, Course course)
{
var instance = new UserCourse
{
UserId = userId,
User = user,
CourseId = coursId,
Course = course
};
return instance;
}
}
This is my database context
public sealed class DatabaseContext : IdentityDbContext<User>, IDatabaseContext
{
public static readonly LoggerFactory MyLoggerFactory
= new LoggerFactory(new[] {new ConsoleLoggerProvider((_, __) => true, true)});
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLoggerFactory(MyLoggerFactory) // Warning: Do not create a new ILoggerFactory instance each time
.EnableSensitiveDataLogging();
public new DbSet<User> Users { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<Lesson> Lessons { get; set; }
public DbSet<UserCourse> UserCourses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Lesson>()
.HasOne(p => p.Course)
.WithMany(b => b.Lessons)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UserCourse>()
.HasKey(uc => new {uc.UserId, uc.CourseId});
modelBuilder.Entity<UserCourse>()
.HasOne(uc => uc.User)
.WithMany(u => u.UserCourses)
.HasForeignKey(uc => uc.UserId);
modelBuilder.Entity<UserCourse>()
.HasOne(uc => uc.Course)
.WithMany(c => c.UserCourses)
.HasForeignKey(uc => uc.CourseId);
}
}
This is the repository where I try to add the 2 existing entities user and course to the joining table with the method AddCoursToProfessor
public class CoursesRepository : ACrudRepository<Course, Guid>, ICoursesRepository
{
public CoursesRepository(IDatabaseContext databaseContext) : base(databaseContext)
{
}
public override IReadOnlyList<Course> GetAll() => _databaseContext.Courses.Include(c => c.Lessons).Include(p => p.UserCourses).ToList();
public override Course GetById(Guid id)
=> _databaseContext.Courses.Include(c => c.Lessons).AsNoTracking().FirstOrDefault(c => c.Id.Equals(id));
public void AddCoursToProfessor(string profId, Guid coursId)
{
var professor = _databaseContext.Users.Include(u => u.UserCourses).FirstOrDefault(u => u.Id.Equals(profId));
var course = GetById(coursId);
var profCourse = UserCourse.CreateUserCourse(profId, professor, coursId, course);
professor.Update(profCourse);
_databaseContext.Users.Update(professor);
_databaseContext.SaveChanges();
}
}
The problem seems to be that when trying to add in the joining table, EF tries to add the course entity into it's table again and I get primary key violation error. I tried different approaches and none of them seem to work. I tried adding directly in UserCourse table but that would try to add both entities into their own tables, I tried deleting the entities before adding them to the join table, that didn't work either. I ran out of ideas, if someone has other ideas, or dealt with similar situations that would be much of help.
I forgot to mention that if I try to add a course by it self or an user, that would work, they both would be added to their tables, so I don't think that the problem is with the DB but with the configuration of the many-to-many relatonship
Ok... so I recently solved the problem. The problem was generated by the GetCourseById method because I was getting the entity AsNoTracking. The entity was not under the EF scope so it tried to create it.
As far as I know here's how I implement a MTM model.
In this instance, there are 3 parameters to take note.
CurrencyPairId is not unique. its a 2-unique parameter.
CurrencyId and IsMain is unique.
This is a trading exchange-styled composite key. i.e. EURUSD
EUR => Main Pair, USD => Counter Pair.
The class
/// <summary>
/// Partial currency pair.
/// </summary>
public class PartialCurrencyPair
{
public long CurrencyId { get; set; }
public long CurrencyPairId { get; set; }
public bool IsMain { get; set; } = false;
public CurrencyPair CurrencyPair { get; set; }
public Currency Currency { get; set; }
}
Currency
public class Currency : BaseEntityModel
{
[Key]
public long Id { get; set; }
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
}
CurrencyPair
public class CurrencyPair : BaseEntityModel
{
[Key]
public long Id { get; set; }
// =========== RELATIONS ============ //
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
}
Some of the mappings
in Currency;
entity.HasMany(c => c.PartialCurrencyPairs).WithOne(pcp => pcp.Currency).HasForeignKey(pcp => pcp.CurrencyId).OnDelete(DeleteBehavior.Restrict);
in CurrencyPair
entity.HasMany(cp => cp.PartialCurrencyPairs).WithOne(pcp => pcp.CurrencyPair).HasForeignKey(pcp => pcp.CurrencyPairId).OnDelete(DeleteBehavior.Restrict);
PartialCurrencyPair
builder.Entity<PartialCurrencyPair>(entity =>
{
entity.HasKey(pcp => new { pcp.CurrencyPairId, pcp.IsMain }).HasName("PartialCurrencyPair_CK_CurrencyPairId_IsMain");
});
Judging by your Course.cs, there's no Collection for UserCourse and why is it static?
Update, 2021 APR 6
EF Core has official documentation for the current practise for implementing MTM.
https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#many-to-many

Entity Framework Core Query Specific Model both directions

Let me preface this question with, I am VERY new to ASP.NET Core/EF Core.
My model look like this:
namespace MyProject.Models
{
public class DeviceContext : DbContext
{
public DeviceContext(DbContextOptions<DeviceContext> options) : base(options) { }
public DbSet<Device> Devices { get; set; }
public DbSet<DeviceLocation> DeviceLocations { get; set; }
}
public class Device
{
public int Id { get; set; }
public string DeviceName { get; set; }
public string ServerName { get; set; }
public string MacAddress { get; set; }
public string LastUpdate { get; set; }
public string WiredIPAddress { get; set; }
public string WirelessIPAddress { get; set; }
public DeviceLocation DeviceLocation { get; set; }
}
public class DeviceLocation
{
public int Id { get; set; }
public string Location { get; set; }
public virtual ICollection<Device> Devices { get; set; }
}
}
I would like to be able to fetch a specific Device based on DeviceName, but I would also like to fetch ALL the devices in a particular Location.
I think the following would work for the first question:
var _Devices = DeviceContext.Devices.FirstOrDefault(d => d.DeviceName == "BLA");
I am just having a hard time getting the second query to run. Ideally, the output would be rendered to JSON to be consumed by an API. I would like the output to look something like this:
{
"Locations": {
"NYC": ["ABC", "123"],
"Boston": ["DEF", "456"],
"Chicago": ["GHI", "789"]
}
}
UPDATE
If I use the following code, it give me the following error:
Code:
// Grouping by ProfileName
var devices = DeviceContext.DeviceLocations.Include(n => n.Device).ToList();
var result = new { success = true, message = "Successfully fetched Devices", data = devices };
return JsonConvert.SerializeObject(result);
Error:
Additional information: Self referencing loop detected for property 'DeviceLocation' with type 'Project.Models.DeviceLocation'. Path 'data[0].Device[0]'.
You can try as shown below.
Note : Use Eager Loading with Include.
using System.Data.Entity;
var devicesList = DeviceContext.DeviceLocations.Where(d=>d.Location = "Your-Location-Name")
.Include(p => p.Devices)
.ToList();
Update :
var devicesList = DeviceContext.DeviceLocations
.Include(p => p.Devices)
.ToList();

How to join my tables with identity tables?

I started a default MVC project with Identity and EF.
In my app users will be able to create and edit some records.
In the table for these records, I want to have the ids of users who created the record and who updated lastly.
My model class is like:
public class Record
{
public int ID { get; set; }
public DateTime CreateTime { get; set; }
public string CreatingUserID { get; set; }
public string UpdatingUserID { get; set; }
public DateTime UpdateTime { get; set; }
public Enums.RecordStatus Status { get; set; }
}
And in RecordsController, I save new records to db like this:
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(FormCollection form, RecordCreateVM vm)
{
string userId = User.Identity.GetUserId();
DateTime now = DateTime.Now;
Record rec = new Record ();
if (ModelState.IsValid)
{
int newRecordId;
using (RecordRepository wr = new RecordRepository())
{
UpdateModel(rec);
rec.CreateTime = now;
rec.UpdateTime = now;
rec.CreatingUserID = userId;
rec.UpdatingUserID = userId;
rec.Status = Enums.RecordStatus.Active;
Record result = wr.Add(rec);
wr.SaveChanges();
newRecordId = result.ID;
}
}
}
When I am listing these records, I also want my page to display these users' usernames.
I get all the active records from the repository I created.
public ActionResult Index()
{
RecordListVMviewModel = new RecordListVM();
using (RecordRepository wr = new (RecordRepository())
{
viewModel.Records = wr.GetAll();
}
return View(viewModel);
}
And this is the repository code:
public class RecordRepository: Repository<Record>
{
public override List<Record> GetAll()
{
IQueryable<Record> activeRecords = DbSet.Where(w => w.Status == Enums.RecordStatus.Active);
return activeRecords.ToList();
}
}
Where do I have to make changes? Can you give me an sample code for usages like this?
Thank you.
You need to change
public string CreatingUserID { get; set; }
public string UpdatingUserID { get; set; }
to something like:
public User CreatingUser { get; set; }
public User UpdatingUser { get; set; }
Set the ID's during the creation of new RecordRepository()
Then access them as Record.CreatingUser.FirstName ect

Using DTO's with OData & Web API

Using Web API and OData, I have a service which exposes Data Transfer Objects instead of the Entity Framework entities.
I use AutoMapper to transform the EF Entities into their DTO counter parts using ProjectTo():
public class SalesOrdersController : ODataController
{
private DbContext _DbContext;
public SalesOrdersController(DbContext context)
{
_DbContext = context;
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get(ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config);
}
}
AutoMapper (V4.2.1) is configured as follows, note the ExplicitExpansion() which prevents serialisation auto expanding navigation properties when they are not requested:
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
cfg.CreateMap<SalesOrderLine, SalesOrderLineDto>()
.ForMember(dest => dest.MasterStockRecord, opt => opt.ExplicitExpansion())
.ForMember(dest => dest.SalesOrderHeader, opt => opt.ExplicitExpansion());
ExplicitExpansion() then creates a new problem where the following request throws an error:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
The query specified in the URI is not valid. The specified type member 'SalesOrderLines' is not supported in LINQ to Entities
The navigation property SalesOrderLines is unknown to EF so this error is pretty much what I expected to happen. The question is, how do I handle this type of request?
The ProjectTo() method does have an overload that allows me to pass in an array of properties that require expansion, I found & modified the extension method ToNavigationPropertyArray to try and parse the request into a string array:
[EnableQuery]
public IQueryable<SalesOrderDto> Get([FromODataUri] string key, ODataQueryOptions<SalesOrderDto> queryOptions)
{
return _DbContext.SalesOrders.Where(so => so.SalesOrderNumber == key)
.ProjectTo<SalesOrderDto>(AutoMapperConfig.Config, null, queryOptions.ToNavigationPropertyArray());
}
public static string[] ToNavigationPropertyArray(this ODataQueryOptions source)
{
if (source == null) { return new string[]{}; }
var expandProperties = string.IsNullOrWhiteSpace(source.SelectExpand?.RawExpand) ? new List<string>().ToArray() : source.SelectExpand.RawExpand.Split(',');
for (var expandIndex = 0; expandIndex < expandProperties.Length; expandIndex++)
{
// Need to transform the odata syntax for expanding properties to something EF will understand:
// OData may pass something in this form: "SalesOrderLines($expand=MasterStockRecord)";
// But EF wants it like this: "SalesOrderLines.MasterStockRecord";
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(" ", "");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace("($expand=", ".");
expandProperties[expandIndex] = expandProperties[expandIndex].Replace(")", "");
}
var selectProperties = source.SelectExpand == null || string.IsNullOrWhiteSpace(source.SelectExpand.RawSelect) ? new List<string>().ToArray() : source.SelectExpand.RawSelect.Split(',');
//Now do the same for Select (incomplete)
var propertiesToExpand = expandProperties.Union(selectProperties).ToArray();
return propertiesToExpand;
}
This works for expand, so now I can handle a request like the following:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines
or a more complicated request like:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($expand=MasterStockRecord)
However, more complicated request that try to combine $select with $expand will fail:
/odatademo/SalesOrders('123456')?$expand=SalesOrderLines($select=OrderQuantity)
Sequence contains no elements
So, the question is: am I approaching this the right way?
It feels very smelly that I would have to write something to parse and transform the ODataQueryOptions into something EF can understand.
It seems this is a rather popular topic:
odata-expand-dtos-and-entity-framework
how-to-specify-the-shape-of-results-with-webapi2-odata-with-expand
web-api-queryable-how-to-apply-automapper
how-do-i-map-an-odata-query-against-a-dto-to-another-entity
While most of these suggest using ProjectTo, none seem to address serialisation auto expanding properties, or how to handle expansion if ExplictExpansion has been configured.
Classes and Config below:
Entity Framework (V6.1.3) entities:
public class SalesOrderHeader
{
public string SalesOrderNumber { get; set; }
public string Alpha { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLine> SalesOrderLines { get; set; }
}
public class SalesOrderLine
{
public string SalesOrderNumber { get; set; }
public string OrderLineNumber { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderHeader SalesOrderHeader { get; set; }
public virtual MasterStockRecord MasterStockRecord { get; set; }
}
public class MasterStockRecord
{
public string ProductCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData (V6.13.0) Data Transfer Objects:
public class SalesOrderDto
{
[Key]
public string SalesOrderNumber { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public virtual ICollection<SalesOrderLineDto> SalesOrderLines { get; set; }
}
public class SalesOrderLineDto
{
[Key]
[ForeignKey("SalesOrderHeader")]
public string SalesOrderNumber { get; set; }
[Key]
public string OrderLineNumber { get; set; }
public string LineType { get; set; }
public string Product { get; set; }
public string Description { get; set; }
public decimal OrderQuantity { get; set; }
public virtual SalesOrderDto SalesOrderHeader { get; set; }
public virtual StockDto MasterStockRecord { get; set; }
}
public class StockDto
{
[Key]
public string StockCode { get; set; }
public string Description { get; set; }
public decimal Quantity { get; set; }
}
OData Config:
var builder = new ODataConventionModelBuilder();
builder.EntitySet<StockDto>("Stock");
builder.EntitySet<SalesOrderDto>("SalesOrders");
builder.EntitySet<SalesOrderLineDto>("SalesOrderLines");
I have created an Automapper explicit navigation expansion utility function that should work with N-deph expands. Posting it here since it might help someone.
public List<string> ProcessExpands(IEnumerable<SelectItem> items, string parentNavPath="")
{
var expandedPropsList = new List<String>();
if (items == null) return expandedPropsList;
foreach (var selectItem in items)
{
if (selectItem is ExpandedNavigationSelectItem)
{
var expandItem = selectItem as ExpandedNavigationSelectItem;
var navProperty = expandItem.PathToNavigationProperty?.FirstSegment?.Identifier;
expandedPropsList.Add($"{parentNavPath}{navProperty}");
//go recursively to subproperties
var subExpandList = ProcessExpands(expandItem?.SelectAndExpand?.SelectedItems, $"{parentNavPath}{navProperty}.");
expandedPropsList = expandedPropsList.Concat(subExpandList).ToList();
}
}
return expandedPropsList;
}
You can call it with :
var navExp = ProcessExpands(options?.SelectExpand?.SelectExpandClause?.SelectedItems)
it will return a list with ["Parent" ,"Parent.Child"]
I never really managed to work this one out. The ToNavigationPropertyArray() extension method helps a little, but does not handle infinite depth navigation.
The real solution is to create Actions or Functions to allow clients to request data requiring a more complicated query.
The other alternative is to make multiple smaller/simple calls then aggregate the data on the client, but this isn't really ideal.
When you want to mark something for explicit expansion in AutoMapper, you need to also opt-back-in when calling ProjectTo<>().
// map
cfg.CreateMap<SalesOrderHeader, SalesOrderDto>()
.ForMember(dest => dest.SalesOrderLines, opt => opt.ExplicitExpansion());
// updated controller
[EnableQuery]
public IQueryable<SalesOrderDto> Get()
{
return _dbContext.SalesOrders
.ProjectTo<SalesOrderDto>(
AutoMapperConfig.Config,
so => so.SalesOrderLines,
// ... additional opt-ins
);
}
While the AutoMapper wiki does state this, the example is perhaps a little misleading by not including the paired ExplicitExpansion() call.
To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand:

Resources