DotNetCore Entity Framework | Generic Pagination - entity-framework

I was looking for a way to do generic pagination with Entity Framework in Dotnet Core 1.1.
I found this guide on MSDN: https://learn.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application
But this was not generic and did not allow me to reuse code.
Included is the answer I used if anyone is looking into this, I thought it would be nice to share.
It uses custom Attributes on models, and returns a pagination model.

EDIT:
The answer below is not correct due to the orderBy not translating into L2E correctly. All the records will be retrieved and sorted in memory what results into poor performance. Check comments for more information and posisble solution.
ORIGNAL:
My solution:
Model.cs:
public class User
{
// Sorting is not allowed on Id
public string Id { get; set; }
[Sortable(OrderBy = "FirstName")]
public string FirstName { get; set; }
}
SortableAttribute.cs:
public class SortableAttribute : Attribute
{
public string OrderBy { get; set; }
}
PaginationService.cs:
public static class PaginationService
{
public static async Task<Pagination<T>> GetPagination<T>(IQueryable<T> query, int page, string orderBy, bool orderByDesc, int pageSize) where T : class
{
Pagination<T> pagination = new Pagination<T>
{
TotalItems = query.Count(),
PageSize = pageSize,
CurrentPage = page,
OrderBy = orderBy,
OrderByDesc = orderByDesc
};
int skip = (page - 1) * pageSize;
var props = typeof(T).GetProperties();
var orderByProperty = props.FirstOrDefault(n => n.GetCustomAttribute<SortableAttribute>()?.OrderBy == orderBy);
if (orderByProperty == null)
{
throw new Exception($"Field: '{orderBy}' is not sortable");
}
if (orderByDesc)
{
pagination.Result = await query
.OrderByDescending(x => orderByProperty.GetValue(x))
.Skip(skip)
.Take(pageSize)
.ToListAsync();
return pagination;
}
pagination.Result = await query
.OrderBy(x => orderByProperty.GetValue(x))
.Skip(skip)
.Take(pageSize)
.ToListAsync();
return pagination;
}
}
Pagination.cs (model):
public class Pagination<T>
{
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
public int TotalItems { get; set; }
public string OrderBy { get; set; }
public bool OrderByDesc { get; set; }
public List<T> Result { get; set; }
}
UserController.cs (inside controller), context is EntityFramework context:
[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery] string orderBy, [FromQuery] bool orderByDesc, [FromQuery] int page, [FromQuery] int size)
{
var query = _context.User.AsQueryable();
try
{
var list = await PaginationService.GetPagination(query, page, orderBy, orderByDesc, size);
return new JsonResult(list);
}
catch (Exception e)
{
return new BadRequestObjectResult(e.Message);
}
}
I hope this helps someone in the future !

Related

EF Core 6 updated data retrieve problem with multiple repositories

I am working on a Blazor Server Application that has a Radzen master-detail data grid. This data grid is populated with IsActive = 1 data OnInitializedAsync method.
Here is the Order repository and related query which retrieves active data:
namespace IMS.Plugins.EFCore
{
public class OrderRepository : IOrderRepository
{
private readonly IMSContext _db;
public OrderRepository(IMSContext db)
{
_db = db;
}
public async Task<IEnumerable<Order?>> GetAllOrders(ClaimsPrincipal user)
{
if (user.IsInRole("Administrators"))
{
return await _db.Orders.Include(d => d.OrderDetails.Where(od => od.IsActive == 1)).ThenInclude(v => v.Vendor).ToListAsync();
}
return await _db.Orders.Include(d => d.OrderDetails.Where(od => od.IsActive == 1)).ThenInclude(v => v.Vendor).ToListAsync();
}
}
}
Here is the Detail repository which sets the related order detail to IsActive = 0
namespace IMS.Plugins.EFCore
{
public class OrderDetailRepository : IOrderDetailRepository
{
private readonly IMSContext _db;
public OrderDetailRepository(IMSContext db)
{
_db = db;
}
public async Task PassiveOrderDetailAsync(OrderDetail orderDetail)
{
var detail = await this._db.OrdersDetail.FindAsync(orderDetail.Id);
if (detail != null)
{
detail.IsActive = 0; // 0-Passive
await _db.SaveChangesAsync();
}
}
}
}
Here is the master-detail data grid populated OnInitializedAsync method. By the way, this part is working. (gets IsActive = 1)
protected override async Task OnInitializedAsync()
{
user = (await _authenticationStateProvider.GetAuthenticationStateAsync()).User;
//userName = user.Identity.Name;
if (!user.Identity.IsAuthenticated)
{
NavigationManager.NavigateTo("/Identity/Account/Login", false);
}
_orders = await ViewAllOrdersUseCase.ExecuteAsync(user);
SelectedOrders = new List<Order?> { _orders.FirstOrDefault() };
_vendors = await ViewAllVendorsUseCase.ExecuteAsync();
_customers = await ViewAllCustomersUseCase.ExecuteAsync();
}
The problem starts when I try to update a detail to IsActive = 0 as shown on the screenshot.
Related Blazor:
<RadzenButton Icon="delete" ButtonStyle="ButtonStyle.Danger" Class="m-1" Click="#(args => PassiveDetail(detail))">
</RadzenButton>
Here is what I do in the related portion:
RadzenDataGrid<OrderDetail> _gridDetail;
IEnumerable<Order?> _orders = new List<Order?>();
...
async Task PassiveDetail(OrderDetail orderDetail)
{
if (orderDetail == _detailToInsert)
{
_detailToInsert = null;
}
await _gridDetail.UpdateRow(orderDetail);
await PassiveOrderDetailUseCase.ExecuteAsync(orderDetail);
_orders = await ViewAllOrdersUseCase.ExecuteAsync(user);
StateHasChanged();
}
Selected row updated successfully but when this line calls _orders = await ViewAllOrdersUseCase.ExecuteAsync(user); it still gets the old data IsActive = 0. I couldn't find out why? However, OnInitializedAsync method works as excepted. Frankly, I couldn't solve.
Edit 1
Is it because there are 2 separate repositories for both order and order details? They are having their own insert and updates?
Edit 2
I should have added earlier Order and OrderDetail entities:
public class Order
{
public int Id { get; set; }
[Required]
public DateTime OrderDateTime { get; set; }
[Required]
[MaxLength(250)]
public int CustomerId { get; set; }
public string Status { get; set; }
[MaxLength(50)]
public string DoneBy { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
public Customer Customer { get; set; }
}
public class OrderDetail
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string ProductCode { get; set; }
[Required]
[MaxLength(250)]
public string ProductName { get; set; }
[Required]
public int Quantity { get; set; }
[Required]
public double BuyUnitPrice { get; set; }
public double CostRatio { get; set; }
public double UnitCost { get; set; }
public double TotalBuyPrice { get; set; }
public double? SellUnitPrice { get; set; }
public double? TotalSellPrice { get; set; }
[MaxLength(150)]
public string? ShippingNumber { get; set; }
public string? Status { get; set; }
[MaxLength(150)]
public string? TrackingNumber { get; set; }
[MaxLength(400)]
public string? Description { get; set; }
public string? Currency { get; set; }
public string? CustomerStockCode { get; set; }
public string? CustomerOrderNumber { get; set; }
public int IsActive { get; set; }
public double? TotalUnitCost { get; set; }
public int OrderId { get; set; }
public int VendorId { get; set; }
public Order Order { get; set; }
public Vendor Vendor { get; set; }
}
Edit 3
I think I found the suspect. The query below shouldn't get the IsActive=0 but it somehow gets! Any ideas for this situation?
Here is the query:
public async Task<IEnumerable<Order?>> GetAllOrders(ClaimsPrincipal user)
{
if (user.IsInRole("Administrators"))
{
return await _db.Orders.Include(d => d.OrderDetails.Where(od => od.IsActive == 1)).ThenInclude(v => v.Vendor).ToListAsync();
}
return await _db.Orders.Include(d => d.OrderDetails.Where(od => od.IsActive == 1)).ThenInclude(v => v.Vendor).ToListAsync();
}
The record is updated, why is the query doesn't work the way it is expected? First I am updating then I am querying IsActive = 1
await PassiveOrderDetailUseCase.ExecuteAsync(orderDetail); //sets to IsActive = 0
_orders = await ViewAllOrdersUseCase.ExecuteAsync(user); // calls GetAllOrders method above,should get only the actives IsActive = 1
But IsActive = 0 also comes inside of _orders as seen in the screenshot below.
Edit 4
When I add AsNoTracking() the query in my previous post gets only IsActive = 1 that is what I want but somehow doesn't get the Customer which is why I want to include it. Order have 2 children, Customer and OrderDetail. OrderDetail has one, Vendor. Couldn't manage to include Customer tough to the query.
This works so far.
Orders
.Include(d => d.OrderDetails.Where(od => od.IsActive == 1))
.ThenInclude(v => v.Vendor)
.Include(c => c.Customer)
.AsNoTracking()
.ToListAsync();

AutoMapper with EF Core doesn't return OData #count correctly if using $top

I'm using the solution provided by this link: AutoMapper don't work with entity EF Core
My problem is when using $top, #odata.count always return the number informed in $top but should return the number of total record.
I know that ODataQueryOptions has a property “Count”, but I don't know if it's possible to use to solve the problem
I'm using the below the code provided by Дмитрий Краснов in his question including the solution by Ivan Stoev:
There is entities:
public class LessonCatalog {
public string Name { get; set; }
public int? ImageId { get; set; }
public virtual Image Image { get; set; }
public virtual ICollection<Lesson> Lessons { get; set; }
}
public class Lesson {
public string Name { get; set; }
public string Description { get; set; }
public int? ImageId { get; set; }
public virtual Image Image { get; set; }
public int LessonCatalogId { get; set; }
public virtual LessonCatalog LessonCatalog { get; set; }
}
Views:
public class LessonView {
public string Name { get; set; }
public string Description { get; set; }
public int? ImageId { get; set; }
public ImageView Image { get; set; }
public int LessonCatalogId { get; set; }
public LessonCatalogView LessonCatalog { get; set; }
}
public class LessonCatalogView {
public string Name { get; set; }
public int? ImageId { get; set; }
public ImageView Image { get; set; }
public IEnumerable<LessonView> Lessons { get; set; }
}
My maps:
CreateMap<LessonCatalog, LessonCatalogView>()
.ForMember(dest => dest.Image, map => map.ExplicitExpansion())
.ForMember(dest => dest.Lessons, map => map.ExplicitExpansion());
CreateMap<Lesson, LessonView>()
.ForMember(dest => dest.LessonCatalog, map => map.ExplicitExpansion());
In my repository:
protected readonly DbContext _context;
protected readonly DbSet<TEntity> _entities;
public Repository(DbContext context) {
_context = context;
_entities = context.Set<TEntity>();
}
public IEnumerable<TView> GetOData<TView>(ODataQueryOptions<TView> query,
Expression<Func<TEntity, bool>> predicate = null) {
IQueryable<TEntity> repQuery = _entities.AsQueryable();
IQueryable res;
if (predicate != null) repQuery = _entities.Where(predicate);
if (query != null) {
string[] expandProperties = GetExpands(query);
//!!!
res = repQuery.ProjectTo<TView>(Mapper.Configuration, null, expandProperties);
//!!!
var settings = new ODataQuerySettings();
var ofilter = query.Filter;
var orderBy = query.OrderBy;
var skip = query.Skip;
var top = query.Top;
if (ofilter != null) res = ofilter.ApplyTo(res, settings);
if (orderBy != null) res = orderBy.ApplyTo(res, settings);
if (skip != null) res = skip.ApplyTo(res, settings);
if (top != null) res = top.ApplyTo(res, settings);
} else {
res = repQuery.ProjectTo<TView>(Mapper.Configuration);
}
return (res as IQueryable<TView>).AsEnumerable();
}
If my query result has 1007 records, and I use
…$count=true&$top=5
the result for count should be
"#odata.count": 1007
But instead the result is always
"#odata.count": 5
Using SQL Server Profile I can see that the Select for count is including the “top”. So, how to avoid this to happen?
I received a help from the Github Guy (thanks to #Gennady Pundikov) and could now answer this question.
I changed the GetOData Method to get the Count before apply others settings:
public IEnumerable<TView> GetOData<TView>(ODataQueryOptions<TView> query,
Expression<Func<TEntity, bool>> predicate = null) {
IQueryable<TEntity> repQuery = _entities.AsQueryable();
IQueryable res;
if (predicate != null) repQuery = _entities.Where(predicate);
if (query != null) {
string[] expandProperties = GetExpands(query);
//!!!
res = repQuery.ProjectTo<TView>(Mapper.Configuration, null, expandProperties);
//!!!
var settings = new ODataQuerySettings();
var ofilter = query.Filter;
var orderBy = query.OrderBy;
var skip = query.Skip;
var top = query.Top;
if (ofilter != null) res = ofilter.ApplyTo(res, settings);
if (query.Count?.Value == true)
{
// We should calculate TotalCount only with filter
// http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part2-url-conventions.html#_Toc371341773
// 4.8 Addressing the Count of a Collection
// "The returned count MUST NOT be affected by $top, $skip, $orderby, or $expand.
query.Request.ODataFeature().TotalCount = ((IQueryable<TView>)res).LongCount();
}
if (top != null) res = top.ApplyTo(res, settings);
if (orderBy != null) res = orderBy.ApplyTo(res, settings);
if (skip != null) res = skip.ApplyTo(res, settings);
} else {
res = repQuery.ProjectTo<TView>(Mapper.Configuration);
}
return (res as IQueryable<TView>).AsEnumerable();
}

Web API Get with query parameters on Mongodb collection

I have WebAPI written and it is using mongodb collection as a database.
it looks like
[
{
"Id":"5a449c148b021b5fb4cb1f66",
"airline":[
{
"airlineID":-1,
"airlineName":"Unknown",
"airlineAlias":"",
"airlineIATACode":"-",
"airlineICAOCode":"N/A",
"airlineCallsign":"",
"airlineBaseCountry":"",
"airlineActiveIndicator":"Y"
},
{
"airlineID":1,
"airlineName":"Private flight",
"airlineAlias":"",
"airlineIATACode":"1T",
"airlineICAOCode":"N/A",
"airlineCallsign":"",
"airlineBaseCountry":"",
"airlineActiveIndicator":"Y"
},
{
"airlineID":2,
"airlineName":"135 Airways",
"airlineAlias":"",
"airlineIATACode":"2T",
"airlineICAOCode":"GNL",
"airlineCallsign":"GENERAL",
"airlineBaseCountry":"United States",
"airlineActiveIndicator":"N"
}
]
}
]
I'm trying to get data using airlineIATACode attribute
public airlineModel Get(string i)
{
_collection = _db.GetCollection<airlineModel>("airline");
var res = Query<airlineModel>.EQ(p => p.airline[0].airlineIATACode, i);
return _collection.FindOne(res);
}
My controller implementation
public HttpResponseMessage Get(string IATAcode)
{
var result = objds.Get(IATAcode);
if (result != null)
return Request.CreateResponse(HttpStatusCode.OK, result);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Data not found");
}
My Model class:
public class airlineModel
{
public ObjectId Id { get; set; }
[BsonElement("airline")]
public List<airlinedata> airline { get; set; }
}
public class airlinedata
{
[BsonElement("airlineID")]
public int airlineID { get; set; }
[BsonElement("airlineName")]
public string airlineName { get; set; }
[BsonElement("airlineAlias")]
public string airlineAlias { get; set; }
[BsonElement("airlineIATACode")]
public string airlineIATACode { get; set; }
[BsonElement("airlineICAOCode")]
public string airlineICAOCode { get; set; }
[BsonElement("airlineCallsign")]
public string airlineCallsign { get; set; }
[BsonElement("airlineBaseCountry")]
public string airlineBaseCountry { get; set; }
[BsonElement("airlineActiveIndicator")]
public string airlineActiveIndicator { get; set; }
}
When I run app and browse http://localhost:60387/api/airlineAPI?IATAcode=1T
it says, Data not found
What can I do to solve this problem?
You can try this.
public airlinedata Get(string i)
{
var _collection = database.GetCollection<airlineModel>("airline");
var filter = Builders<airlineModel>.Filter
.ElemMatch(model => model.airline, airline => airline.airlineIATACode == i);
var projection = Builders<airlineModel>.Projection
.Include(model => model.airline[-1]);
var airlineModel = _collection.Find(filter)
.Project<airlineModel>(projection)
.Single();
return airlineModel.airline.Single();
}
Note that you don't need to put mapping attributes on each field. The default mapping does exactly what you did with attributes.
The only attribute I suggest you to use is [BsonId] to tell MongoDB which is your _id field.
public class airlineModel
{
[BsonId(IdGenerator = typeof(ObjectIdGenerator))]
public ObjectId Id { get; set; }
...
}
Finally, in case of a large collection, don't forget to create an index on airlineIATACode field, otherwise the search would perform an expensive COLLSCAN.

EF6: Single relationship table for multiple related entities

I have a EF Model with many entities, like Nodes, Attributes, Tags, etc.
There is also an "Alias" entity, and pretty much every other entity else can have a many-to-many relationship with Aliases. One of the undesired things about this is the number of tables that are created to track these relationships (eg. NodeAlias, AttributeAlias, etc.).
Are there any design alternatives that could map an Alias to all of the other entities in a single table? I was thinking maybe something along these lines if it's possible:
+---------+--------+-------------+-----------+
| AliasId | NodeId | AttributeId | TagId |
+---------+--------+-------------+-----------+
| 1 | 1 | 2 | 3 |
+---------+--------+-------------+-----------+
I updated my solution to provide many-to-many relationships between aliases and every other entity.
I intentionally posted this as a separate answer so that my previous answer can also remain here if anyone would need it.
Step #1: I created extension methods for getting and setting property values using reflection in a convenient way:
public static class ObjectExtensions
{
public static TResult GetPropertyValue<TResult>(this object entity, string propertyName)
{
object propertyValue = entity?.GetType().GetProperty(propertyName)?.GetValue(entity);
try
{
return (TResult)propertyValue;
}
catch
{
return default(TResult);
}
}
public static void SetPropertyValue(this object entity, string propertyName, object value)
{
entity?.GetType().GetProperty(propertyName)?.SetValue(entity, value);
}
}
Step #2: I updated the models to provide many-to-many relationship.
public class Node
{
[Key]
public int NodeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Attribute
{
[Key]
public int AttributeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class AliasMapping
{
[Key]
public int Id { get; set; }
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int? NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int? AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int? TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3: Due to relationship changes the MyDbContext could have been simplified as the [ForeignKey] data annotations are enough.
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
}
Step #4: I also updated the extension methods so that you can create and remove alias mappings.
public static class AliasExtensions
{
public static void CreateMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
if (mappings.Any(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId))
{
// We already have the mapping between the specified entity and alias.
return;
}
bool usableMappingExists = true;
var usableMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == null);
if (usableMapping == null)
{
usableMappingExists = false;
usableMapping = new AliasMapping()
{
Alias = alias
};
}
usableMapping.SetPropertyValue(mappingEntityPropertyName, entity);
usableMapping.SetPropertyValue(entityKeyPropertyName, entityId);
if (!usableMappingExists)
{
context.AliasMappings.Add(usableMapping);
}
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
public static void RemoveMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
AliasMapping currentMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId);
if (currentMapping == null)
{
// There is no mapping between the specified entity and alias.
return;
}
currentMapping.SetPropertyValue(mappingEntityPropertyName, null);
currentMapping.SetPropertyValue(entityKeyPropertyName, null);
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
}
Step #5: Updated the console app steps to align it with the changes.
class Program
{
static void Main(string[] args)
{
// Consider specify the appropriate database initializer!
// I use DropCreateDatabaseAlways<> strategy only for this example.
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
// Always save changes after adding an entity but before trying to create a mapping.
context.SaveChanges();
// One Alias To Many Entities
context.CreateMapping(nodes[0], aliases[0]);
context.CreateMapping(nodes[1], aliases[0]);
context.CreateMapping(nodes[2], aliases[0]);
context.CreateMapping(nodes[3], aliases[0]);
context.CreateMapping(attributes[0], aliases[0]);
context.CreateMapping(attributes[1], aliases[0]);
context.CreateMapping(attributes[2], aliases[0]);
context.CreateMapping(tags[0], aliases[0]);
context.CreateMapping(tags[1], aliases[0]);
// One Entity To Many Aliases
context.CreateMapping(nodes[4], aliases[0]);
context.CreateMapping(nodes[4], aliases[1]);
context.CreateMapping(nodes[4], aliases[2]);
context.CreateMapping(attributes[3], aliases[1]);
context.CreateMapping(attributes[3], aliases[3]);
context.CreateMapping(tags[2], aliases[2]);
context.CreateMapping(tags[2], aliases[3]);
// Remove mapping
context.RemoveMapping(nodes[4], aliases[0]);
// Not really needed here as both 'CreateMapping' and 'RemoveMapping' save the changes
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}
Please note: RemoveMapping() will not delete an AliasMapping even if no entity is associated with it! But CreateMapping() will make use of it later if needed. E.g. look at the screenshot below and check AliasMapping where Id = 5.
Screenshot about the execution result:
You were talking about many-to-many relationship but reading your post I think it is more likely a "special one-to-many" relationship, actually "combined multiple one-to-one" relationship as I see that an Alias can be mapped to a single Node AND/OR to a single Attribute AND/OR to a single Tag.
I think I found a solution for this case.
If it's not the case and an Alias can be mapped to multiple Node AND/OR to multiple Attribute AND/OR to multiple Tag then I think this solution below needs only a small change. :)
Step #1 - These are my example models
public class Node
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Attribute
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Tag
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
Step #2 - Creating the custom mapping table
public class AliasMapping
{
[Key]
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3 - Creating the DbContext
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Attribute)
.WithOptionalPrincipal(attribute => attribute.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Node)
.WithOptionalPrincipal(node => node.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Tag)
.WithOptionalPrincipal(tag => tag.AliasMapping)
.Map(config => config.MapKey("AliasId"));
}
}
Step #4 - Creating extension method so that creating a relationship will be easy
public static class AliasExtensions
{
public static void CreateMapping<TEntity>(this MyDbContext context, TEntity entity, Alias alias)
{
string mappingEntityPropertyName = typeof(TEntity).Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
bool entityExists = true;
var mapping = context.AliasMappings.Find(alias.AliasId);
if (mapping == null)
{
entityExists = false;
mapping = new AliasMapping()
{
Alias = alias
};
}
typeof(AliasMapping)
.GetProperty(mappingEntityPropertyName)
.SetValue(mapping, entity);
typeof(AliasMapping)
.GetProperty(entityKeyPropertyName)
.SetValue(mapping, typeof(TEntity).GetProperty("Id").GetValue(entity));
if (!entityExists)
{
context.AliasMappings.Add(mapping);
}
}
}
Step #5 - Created a console app to see this working
class Program
{
static readonly Random rnd = new Random(DateTime.Now.TimeOfDay.Milliseconds);
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
context.SaveChanges();
// Associate aliases to attributes
attributes.ForEach(attribute =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Attribute == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(attribute, selectedAlias);
});
// Associate aliases to nodes
nodes.ForEach(node =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Node == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(node, selectedAlias);
});
// Associate aliases to tags
tags.ForEach(tag =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Tag == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(tag, selectedAlias);
});
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}

why the explicit load does not work,and the navigation property always null?

All my code is here,quite simple,and I don't konw where it goes wrong.
Person and Task has an many-to-many relationship.
I want to load someone's task using the explicit way.
I follow the way this post shows,and i can't make it work.
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Task> Tasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public string Subject { get; set; }
public ICollection<Person> Persons { get; set; }
}
public class Ctx : DbContext
{
public Ctx()
: base("test")
{
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
public DbSet<Person> Persons { get; set; }
public DbSet<Task> Task { get; set; }
}
class Program
{
static void Main(string[] args)
{
//add some data as follows
//using (var ctx = new Ctx())
//{
//ctx.Persons.Add(new Person { Name = "haha" });
//ctx.Persons.Add(new Person { Name = "eeee" });
//ctx.Task.Add(new Task { Subject = "t1" });
//ctx.Task.Add(new Task { Subject = "t2" });
//ctx.SaveChanges();
//var p11 = ctx.Persons.FirstOrDefault();
//ctx.Task.Include(p2 => p2.Persons).FirstOrDefault().Persons.Add(p11);
//ctx.SaveChanges();
//}
var context = new Ctx();
var p = context.Persons.FirstOrDefault();
context.Entry(p)
.Collection(p1 => p1.Tasks)
.Query()
//.Where(t => t.Subject.StartsWith("t"))
.Load();
//the tasks should have been loaded,isn't it?but no...
Console.WriteLine(p.Tasks != null);//False
Console.Read();
}
}
Is there anything wrong with my code?I'm really new to EF,so please, someone help me.
The problem is your .Query() call. Instead of loading the collection, you are getting a copy of the IQueryable that would be used to load, then executing the query.
Remove your .Query() line and it will work.
If what you are looking for is getting a filtered list of collection elements, you can do this:
var filteredTasks = context.Entry(p)
.Collection(p1 => p1.Tasks)
.Query()
.Where(t => t.Subject.StartsWith("t"))
.ToList();
This will not set p.Tasks, nor is it a good idea to do so, because you'd be corrupting the domain model.
If you really, really want to do that... this might do the trick (untested):
var collectionEntry = context.Entry(p).Collection(p1 => p1.Tasks);
collectionEntry.CurrentValue =
collectionEntry.Query()
.Where(t => t.Subject.StartsWith("t"))
.ToList();
This solution worked for me :
For some reasons EF requires virtual keyword on navigation property, so the entities should be like this :
public class Person
{
//...
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
//...
public virtual ICollection<Person> Persons { get; set; }
}