Lookup-able models - c#

I'm currently working on a project using MongoDB as it's database and I got some issues on how to solve the problem of having a model containing some ids to foreign documents and looking them up without needing to fall back to BsonDocument.
Originally I had this (stripped down) model class:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
Sometimes I need the concrete TaskConfiguration referenced by TaskId.
In this case I use a $lookup to resolve it.
First I introduced a new member to hold the concrete TaskConfiguration instance changing my model class to:
public class TestSession
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
Then I realized that the aggregation was unable to deserialize the looked-up and unwound Task value, I guess because of BsonIgnore.
I could not find any options to override this and I couldn't come up with any other solution to this.
Finally I decided to split the model class into the initial one containing exactly the database representation of the document and a derived class containing the resolved Task member without the BsonIgnore attribute in order for it to be deserializable.
The resulting classes look like this:
public class TestSessionModel
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("taskId")]
public ObjectId? TaskId { get; set; }
}
public class TestSession : TestSessionModel
{
[BsonElement("task")]
public TaskConfiguration Task { get; set; }
}
Although this approach seems to work I now have to split up all similiar classes which not only creates a lot of classes potentially but also could potentially confuse the reader what class to use.
Additionally one has to be careful to not store the derived classes into the database.
My question now is if my current approach is actually the way to go in this case or if there are better/safer alternatives?

you can do it with the BsonIgnore approach but you need to project the final result like this:
var res = collection.AsQueryable()
.Where(t => t.Id == session.Id)
.Join(
foreignColl.AsQueryable(), // foreign collection
s => s.TaskId, // local field
c => c.Id, // foreign field
(s, c) => new TestSession // projection
{
Id = s.Id,
Name = s.Name,
TaskId = s.TaskId,
Task = c
})
.Single();
here's the full program i used for testing:
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Entities;
using MongoDB.Entities.Core;
using System.Linq;
namespace StackOverflow
{
public class TestSession : Entity
{
public string Name { get; set; }
[BsonRepresentation(BsonType.ObjectId)]
public string TaskID { get; set; }
[BsonIgnore]
public TaskConfiguration Task { get; set; }
}
public class TaskConfiguration : Entity
{
public int NumOfIterations { get; set; }
}
public static class Program
{
private static void Main()
{
new DB("test");
var task = new TaskConfiguration { NumOfIterations = 10 };
task.Save();
var session = new TestSession { Name = "This is a test session", TaskID = task.ID };
session.Save();
var res = DB.Queryable<TestSession>()
.Where(t => t.ID == session.ID)
.Join(
DB.Queryable<TaskConfiguration>(),
s => s.TaskID,
c => c.ID,
(s, c) => new TestSession
{
ID = s.ID,
Name = s.Name,
TaskID = s.TaskID,
Task = c
})
.Single();
}
}
}

Related

Map List of one Type to a list of another similar Type

I have two different classes:
The following is a model for the database table named "Attachments":
namespace WebApi.Models
{
public class Attachment
{
public enum EntityType
{
Transaction
}
public int AttachmentId { get; set; }
public int EntityId { get; set; }
public EntityType Type { get; set; }
public string Filename { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}
}
and in my dbContext class I have something like the following:
namespace WebApi.Models
{
public class accountingContext : DbContext
{
public DbSet<User>? Users { get; set; }
public DbSet<Transaction>? Transactions { get; set; }
public DbSet<Attachment>? Attachments { get; set; }
///...
}
}
and then I have a similar cvlass to the Attachement model which is usedf to send data back in response to a web api request, the class is below:
namespace WebApi.Entities.AttachmentResponse;
public class AttachmentResponse
{
public int AttachmentId { get; set }
public string Filename { get; set; }
}
I wish to keep my class for responses seperate rather then using the model classes for reasons I don't feel worthwhile going into.
After performing a query against the model classes I need to convert the result and map to the AttachmentResponse class. I am trying the following:
List<WebApi.Entities.AttachmentResponse> attachments = (
from a in db.Attachments
from t in db.Transactions.Where(t => a.EntityId == t.TransactionId && t.UserId == userId)
where a.EntityId == EntityId
select new
{
a.AttachmentId,
a.Filename
}).ToList();
But I get an error with the above attempt. I have also looked at ConvertAll but cannot see how to use it in the code above.
Is it possible to accomplish what I am attempting? If so, where am I going wrong above?
You're creating a list of an anonymous type, not a list of AttachmentResponse objects. You need to explicitly create them in the projection:
select new AttachmentResponse
{
AttachmentId = a.AttachmentId,
Filename = a.Filename
}
I would also look at tools like AutoMapper that can create mappings very easily, especially when the property names are the same.
I can't comment, so I can't ask what error you're getting. However, from looking at it, it looks like a casting problem. You can cast anonymous types to other types. Using just new makes is anonymous instead specify they type that you are declaring new.
List<WebApi.Entities.AttachmentResponse> attachments = (
from a in db.Attachments
from t in db.Transactions.Where(t => a.EntityId == t.TransactionId && t.UserId == userId)
where a.EntityId == EntityId
select new WebApi.Entities.AttachmentResponse
{
a.AttachmentId,
a.Filename
}).ToList();

Entity Framework Core (Postgres) Multiple Includes creates ghost property

I'm having an issue with a series of Include/ThenInclude in a query.
Here is my EntityFrameworkCore Query :
var fund = await funds.Where(x => x.Id == fundId)
.Include(f => f.Compositions.Where(compo => compo.Date == compositionDate))
.ThenInclude(c => c.CompositionItems)
.ThenInclude(item => item.Asset)
.FirstOrDefaultAsync(token)
?? throw new NotFoundException(nameof(Fund), fundId);
I recieve a 'CompositionDate does not exists' error.
As you can see the CompositionDate property is at the Compositions Level.
When I check the SQL generated I get this in a subquery Select statement :
SELECT f1."CompositionFundId", f1."CompositionDate", f1."AssetId", f1."Amount", a."Id", a."CountryCode", a."Currency", a."FundCompositionDate", a."FundCompositionFundId", a."Isin", a."Name", a."SecurityType", a."Ticker", a."Coupon", a."GicsSector", a."InvestmentCase", a."IpoDate", a."Theme"
FROM "FundCompositionItem" AS f1
INNER JOIN "Asset" AS a ON f1."AssetId" = a."Id"
Those 2 properties a."FundCompositionDate", a."FundCompositionFundId" doesn't exists at the 'Asset' level.
They exists in the parent (at the 'Where' level on the first Include).
I'm using Postgres provider for EFcore. Could this be the issue?
Should I be using the select anonymous type .Select(x => new { Fund = x, Compo = x.Compo.Where(...), etc... }?
I would like to preserve the navigation properties if possible. (accessing assets from compositionItems)
Any help would be much appreciated.
Edit:
Models as requested by Atiyar:
public class Portfolio : AuditableEntity
{
public Guid Id { get; set; }
public Guid Name{ get; set; }
}
public class Fund : Portfolio
{
// Irrelevant properties
public IList<FundComposition> Compositions { get; } = new List<FundComposition>();
}
public class FundComposition
{
public Fund Fund { get; set; }
// Primary key / Foreign key
public Guid FundId { get; set; }
// Primary Key
public DateTime Date { get; set; }
public List<FundCompositionItem> CompositionItems { get; set; } = new();
}
public class FundCompositionItem
{
public FundComposition Composition { get; set; }
// Primary Key
public Guid CompositionFundId { get; set; }
// Primary Key
public DateTime CompositionDate { get; set; }
public Asset Asset { get; set; }
// Primary Key
public Guid AssetId { get; set; }
public double Amount { get; set; }
}
public class Asset : BaseEntity
{
// Primary Key
public Guid Id { get; set; }
public string Name { get; set; }
public string Currency { get; set; }
// more properties
}
In my experience, I've applied the Include() and ThenInclude() first and then applied the any conditional clauses afterwards. I'm also not sure if using Where inside of an include method does what you expect it to.
You can also apply your conditional in the first parameter of .FirstOrDefaultAsync().
var fund = await funds.Where(x => x.Id == fundId)
.Include(f => f.Compositions)
.ThenInclude(c => c.CompositionItems)
.ThenInclude(item => item.Asset)
.FirstOrDefaultAsync(x =>
x.Id == fundId && x.Compositions.Any(compo => compo.Date == compositionDate),
token
)

How to hide items, in an API reply, from a db query?

I'm currently using MVC with EF to have a small server with API querying a SQL database. But in the API reply I'm not able to hide some parameters.
The main object
public class AssetItem
{
[Key]
public Int32 AssetId { get; set; }
public String AssetName { get; set; }
public int OdForeignKey { get; set; }
[ForeignKey("OdForeignKey")]
public OperationalDataItem OperationalDataItem { get; set; }
}
The other one:
public class OperationalDataItem
{
[Key]
public Int32 OperationalDataId { get; set; }
public String Comunity { get; set; }
public List<AssetItem> AssetItems { get; set; }
}
From what I have read, this should be ok, I have also set the context:
public AssetContext(DbContextOptions<AssetContext> options) : base(options)
{}
public DbSet<AssetItem> AssetItems { get; set; }
public DbSet<OperationalDataItem> OperationalDataItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AssetItem>().HasOne(p =>
p.OperationalDataItem).WithMany(b => b.AssetItems).HasForeignKey(p =>
p.OdForeignKey);
}
And the seeding in program.cs
context.AssetItems.Add(
new AssetItem { AssetName = "Test test", OdForeignKey = 1,
OperationalDataItem =
new OperationalDataItem {Comunity = "Comunity1" }});
So calling the API this results in:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":null }
From what I read this is because of the lazy loading, how can I hide the result operationalDataItem?
In case is not possible i have of course try to query for it and give it back and it give something like:
{ "assetId":3,
"assetName":"Test test",
"odForeignKey":1,
"operationalDataItem":
{ "operationalDataId":1,
"comunity":"Comunity1",
"assetItems":[
But in this case I would like to hide "assetsItems" in the reply to the FE.
How can I hide those parameters?
The API is quite simple, just an example code:
var todoItem = await _context.AssetItems.FindAsync((Int32)id);
var item = _context.OperationalDataItems.Find((Int32)todoItem.OdForeignKey);
todoItem.OperationalDataItem = item;
return todoItem
If you want to fetch data from the database, but you only want to fetch some properties, use Select. Usually this is more efficient than using Find, because you'll only transfer the data that you actually plan to use.
To fetch some properties of the assetItem that has primary key assetItemId:
var result = dbContext.AssetItems
.Where(assetItem => assetItem.AssetItmId = assetItemId)
.Select(assetItem => new
{
// Select only the properties that you plan to use
Id = assetItem.AssertItemId,
Name = assetItem.Name,
OperationalData = new
{
// again, select only the properties that you plan to use
Id = assetItem.OperationalData.OperationalDataId,
Community = assetItem.OperationalData.Community,
},
})
.FirstOrDefault();
Or the other way round:
Fetch several properties of all (or some) OperationalDataItems, each with some properties of all (or some) of its AssetItems:
var result = dbContext.OperqationalDataItems
.Where(operationalDataItem => ...) // only if you don't want all
.Select(operationalDataItem => new
{
Id = operationalDataItem.Id,
Community = operationalDataItem.Community
AssetItems = operationalDataItem.AssetItems
.Where(assetItem => ...) // only if you don't want all its assetItems
.Select(assetItem => new
{
// Select only the properties you plan to use:
Id = assetItem.Id,
...
// not useful: you know the value of the foreign key:
// OperationalDataId = assetItem.OperationalDataId,
})
.ToList();
})
.ToList(); // or: FirstOrDefault if you expect only one element
Entity framework knows your one-to-many relation and is smart enough to know which (group-)join is needed for your query.
Some side remarks
You've declare your many-relation a List<AssetItem>. Are you sure that operationalDataItem.AssetItems[4] has a defined meaning? Wouldn't it be better to stick to the entity framework code first conventions? This would also eliminate the need for most attributes and / or fluent API
public class OperationalDataItem
{
public int Id { get; set; }
public String Comunity { get; set; }
...
// Every OperationalDataItem has zero or more AssetItems (one-to-many)
public virtual ICollection<AssetItem> AssetItems { get; set; }
}
public class AssetItem
{
public int Id { get; set; }
public String Name { get; set; }
...
// every AssetItem belongs to exactly one OperationalDataItem, using foreign key
public int OperationDataItemId { get; set; }
public virtual OperationalDataItem OperationalDataItem { get; set; }
}
In entity framework the columns of a table are represented by the non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many)
Because I stuck to the conventions, no attributes nor fluent API is needed. Entity framework is able to detect the one-to-many relation and the primary and foreign keys. Only if I am not satisfied with the names or the types of the columns I would need fluent API.

Prevent SELECT N+1 issue in Entity Framework query using Joins

I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();

Entity framework relationships

I have these three entities:
public class Dog
{
public int DogId { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public bool Checked { get; set; }
public string DogImage { get; set; }
public virtual ICollection<Result> Results { get; set; }
}
public class Event
{
public int EventId { get; set; }
public string EventName { get; set; }
public string EventLocation { get; set; }
public string EventType { get; set; }
public string EventDate { get; set; }
public virtual ICollection<Result> Results { get; set; }
}
public class Result
{
public int ResultId { get; set; }
public int Track { get; set; }
public int Obedience { get; set; }
public int Protection { get; set; }
[ForeignKey("Dog")]
public int DogId { get; set; }
public virtual Dog Dog { get; set; }
[ForeignKey("Event")]
public int EventId { get; set; }
public virtual Event Event { get; set; }
}
I´ve been getting help from here before in order to set it up like this.
Entity Framework errors when trying to create many-to-many relationship
So the way it is now I guess the result is the "glue" that ties these classes together containing foreign keys to the two other tables.
What I have been trying to achieve for days now is to:
Create an event.
Add dogs to the event.
Add results to the dogs participating in the choosenEvent.
Lets say I create an event like this:
[HttpPost]
public ActionResult CreateEvent(Event newEvent)
{
newEvent.EventDate = newEvent.EventDate.ToString();
_ef.AddEvent(newEvent);
return View();
}
Now I guess the next step would be to add a list of dogs to this event and in order to do that I need to somehow use my result-class since that's the "glue"-class. Please let me know if I'm even on the right track here.
It is not really a good idea to do many to many relationships like how you've done. See here
In order to get a proper many to many relationship, mapped in the proper way in the database, that doesn't have pitfalls, I would try it this way:
public class Dog {}
public class Event {}
public class Result {}
// This is a linking table between Dog and Results
public class DogResult
{
public int Id {get;set;}
public int DogId {get;set;}
public int ResultId {get;set;}
}
// This is a linking table between Events and Results
public class EventResult
{
public int Id {get;set;}
public int EventId {get;set;}
public int ResultId {get;set;}
}
When you now write your query you can do this:
using (var context = new DbContext())
{
var dogs = context.Dogs();
var dogResults = context.DogResults();
var results = context.Results();
var dogsAndResults = dogs.Join(
dogResults,
d => d.Id,
r => r.DogId,
(dog, dogResult) => new { dog, dogResult })
.Join(
results,
a => a.dogResult.ResultId,
r => r.Id,
(anon, result) => new { anon.dog, result });
}
It is a bit nasty looking, but it will give you back a list of anonymous objects containing a Dog and its related Result. But obviously it would be better to do this in a stored proc:
using (var context = new DbContext())
{
var results = context.Database.ExecuteStoreQuery<SomeResultDto>("SELECT * .... JOIN ... ");
}
This is cleaner, because you are using SQL.
This is a more complex way of dealing with it. But far more performant, especially if you understand fully how entity framework executes LINQ.
Obviously if you want to create these links:
using (var context = new DbContext())
{
context.Dogs.AddRange(dogs); // dogs being a list of dog entities
context.Results.AddRange(results); // events being a list of results entities
context.DogResults.AddRange(dogResults); // a list of the links
}
It is completely up to you how you create these links. To turn this into a sproc as well, you want to create some custom User Defined Table Types and use them as a Table Value Parameter.
var dogResults = dogs.SelectMany( d => results.Select ( r => new DogResult { DogId = d.Id, ResultId = r.Id } ) );
That is a beast of a LINQ query and basically it gets every dog and links it to every result. Run it in LinqPad and Dump the values.
I've only done this using the fluent method (when I was learning I found you can do everything in fluent, but not with annotations, so I've not looked into them), the following creates a many to many between my Unit entity and my UnitService entity:
modelBuilder.Entity<Unit>()
.HasMany<UnitService>(u => u.Services)
.WithMany(us => us.Units);
This code is in the protected override void OnModelCreating(DbModelBuilder modelBuilder) method.
In your case Event is Unit and Dog is UnitService.
Oh ooops, you don't need that at all, your 'join' table is your results table, in my case I don't care about the join table so its all hidden.
Maybe something like:
modelBuilder.Entity<Result>()
.HasMany<Event>(e => e.Results);
modelBuilder.Entity<Result>()
.HasMany<Dog>(d => d.Results);

Categories

Resources