Say I have a class, I want to select multiple objects of it but create one unified object in the end. This is because of the requirement for the collection properties of the object to be combined.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using Nozomi.Base.Core;
namespace Nozomi.Data.Models.Currency
{
public class Currency : BaseEntityModel
{
public Currency(ICollection<Currency> currencies)
{
if (currencies.Any())
{
var firstCurr = currencies.FirstOrDefault();
if (firstCurr != null)
{
// Doesn't matter...
Id = firstCurr.Id;
CurrencyTypeId = firstCurr.Id;
CurrencyType = firstCurr.CurrencyType;
Abbrv = firstCurr.Abbrv;
Name = firstCurr.Name;
CurrencySourceId = firstCurr.CurrencySourceId;
CurrencySource = firstCurr.CurrencySource;
WalletTypeId = firstCurr.WalletTypeId;
PartialCurrencyPairs = currencies
.SelectMany(c => c.PartialCurrencyPairs)
.DefaultIfEmpty()
.ToList();
}
}
}
[Key]
public long Id { get; set; }
public long CurrencyTypeId { get; set; }
public CurrencyType CurrencyType { get; set; }
public string Abbrv { get; set; } // USD? MYR? IND?
public string Name { get; set; }
public long CurrencySourceId { get; set; }
public Source CurrencySource { get; set; }
// This will have a number if it is a crypto pair to peg to proper entities
public long WalletTypeId { get; set; } = 0;
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
public bool IsValid()
{
return !String.IsNullOrEmpty(Abbrv) && !String.IsNullOrEmpty(Name) && CurrencyTypeId > 0 && CurrencySourceId > 0;
}
}
}
Here's what a PartialCurrencyPair is:
namespace Nozomi.Data.Models.Currency
{
/// <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; }
}
}
So basically, if you want to make EURUSD, you'll have to take two currencies to form a pair. A CurrencyPair is made up of two PartialCurrencyPairs. The reason why we can have many EUR or many USDs is that they come from different sources.
Here's what a CurrencyPair is:
public class CurrencyPair : BaseEntityModel
{
[Key]
public long Id { get; set; }
public CurrencyPairType CurrencyPairType { get; set; }
/// <summary>
/// Which CPC to rely on by default?
/// </summary>
public string DefaultComponent { get; set; }
public long CurrencySourceId { get; set; }
public Source CurrencySource { get; set; }
// =========== RELATIONS ============ //
public ICollection<CurrencyPairRequest> CurrencyPairRequests { get; set; }
public ICollection<WebsocketRequest> WebsocketRequests { get; set; }
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
public bool IsValid()
{
var firstPair = PartialCurrencyPairs.First();
var lastPair = PartialCurrencyPairs.Last();
return (CurrencyPairType > 0) && (!string.IsNullOrEmpty(APIUrl))
&& (!string.IsNullOrEmpty(DefaultComponent))
&& (CurrencySourceId > 0)
&& (PartialCurrencyPairs.Count == 2)
&& (firstPair.CurrencyId != lastPair.CurrencyId)
&& (!firstPair.IsMain == lastPair.IsMain);
}
}
I have an IQueryable to combine into one single currency.
Code with comments (The comments basically tells you what I'm trying to achieve.
var query = _unitOfWork.GetRepository<Currency>()
.GetQueryable()
// Do not track the query
.AsNoTracking()
// Obtain the currency where the abbreviation equals up
.Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
&& c.DeletedAt == null && c.IsEnabled)
// Something here that will join the PartialCurrencyPair collection together and create one single Currency object.
.SingleOrDefault();
How do I come about it? Thank you so much in forward! Here's the
progress I've made so far and it works, but I'm pretty LINQ has a beautiful way to make this better and optimised:
var combinedCurrency = new Currency(_unitOfWork.GetRepository<Currency>()
.GetQueryable()
// Do not track the query
.AsNoTracking()
// Obtain the currency where the abbreviation equals up
.Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
&& c.DeletedAt == null && c.IsEnabled)
.Include(c => c.PartialCurrencyPairs)
.ThenInclude(pcp => pcp.CurrencyPair)
.ThenInclude(cp => cp.CurrencyPairRequests)
.ThenInclude(cpr => cpr.RequestComponents)
.ThenInclude(rc => rc.RequestComponentDatum)
.ThenInclude(rcd => rcd.RcdHistoricItems)
.ToList());
return new DetailedCurrencyResponse
{
Name = combinedCurrency.Name,
Abbreviation = combinedCurrency.Abbrv,
LastUpdated = combinedCurrency.PartialCurrencyPairs
.Select(pcp => pcp.CurrencyPair)
.SelectMany(cp => cp.CurrencyPairRequests)
.SelectMany(cpr => cpr.RequestComponents)
.OrderByDescending(rc => rc.ModifiedAt)
.FirstOrDefault()?
.ModifiedAt ?? DateTime.MinValue,
WeeklyAvgPrice = combinedCurrency.PartialCurrencyPairs
.Select(pcp => pcp.CurrencyPair)
.Where(cp => cp.CurrencyPairRequests
.Any(cpr => cpr.DeletedAt == null && cpr.IsEnabled))
.SelectMany(cp => cp.CurrencyPairRequests)
.Where(cpr => cpr.RequestComponents
.Any(rc => rc.DeletedAt == null && rc.IsEnabled))
.SelectMany(cpr => cpr.RequestComponents
.Where(rc =>
rc.ComponentType.Equals(ComponentType.Ask) ||
rc.ComponentType.Equals(ComponentType.Bid)))
.Select(rc => rc.RequestComponentDatum)
.SelectMany(rcd => rcd.RcdHistoricItems
.Where(rcdhi => rcdhi.CreatedAt >
DateTime.UtcNow.Subtract(TimeSpan.FromDays(7))))
.Select(rcdhi => decimal.Parse(rcdhi.Value))
.DefaultIfEmpty()
.Average(),
DailyVolume = combinedCurrency.PartialCurrencyPairs
.Select(pcp => pcp.CurrencyPair)
.Where(cp => cp.CurrencyPairRequests
.Any(cpr => cpr.DeletedAt == null && cpr.IsEnabled))
.SelectMany(cp => cp.CurrencyPairRequests)
.Where(cpr => cpr.RequestComponents
.Any(rc => rc.DeletedAt == null && rc.IsEnabled))
.SelectMany(cpr => cpr.RequestComponents
.Where(rc => rc.ComponentType.Equals(ComponentType.VOLUME)
&& rc.DeletedAt == null && rc.IsEnabled))
.Select(rc => rc.RequestComponentDatum)
.SelectMany(rcd => rcd.RcdHistoricItems
.Where(rcdhi => rcdhi.CreatedAt >
DateTime.UtcNow.Subtract(TimeSpan.FromHours(24))))
.Select(rcdhi => decimal.Parse(rcdhi.Value))
.DefaultIfEmpty()
.Sum(),
Historical = combinedCurrency.PartialCurrencyPairs
.Select(pcp => pcp.CurrencyPair)
.SelectMany(cp => cp.CurrencyPairRequests)
.SelectMany(cpr => cpr.RequestComponents)
.Where(rc => componentTypes != null
&& componentTypes.Any()
&& componentTypes.Contains(rc.ComponentType)
&& rc.RequestComponentDatum != null
&& rc.RequestComponentDatum.IsEnabled
&& rc.RequestComponentDatum.DeletedAt == null
&& rc.RequestComponentDatum.RcdHistoricItems
.Any(rcdhi => rcdhi.DeletedAt == null &&
rcdhi.IsEnabled))
.ToDictionary(rc => rc.ComponentType,
rc => rc.RequestComponentDatum
.RcdHistoricItems
.Select(rcdhi => new ComponentHistoricalDatum
{
CreatedAt = rcdhi.CreatedAt,
Value = rcdhi.Value
})
.ToList())
};
Here's the end result I want on that single object: A DetailedCurrencyResponse object.
public class DistinctiveCurrencyResponse
{
public string Name { get; set; }
public string Abbreviation { get; set; }
public DateTime LastUpdated { get; set; }
public decimal WeeklyAvgPrice { get; set; }
public decimal DailyVolume { get; set; }
}
A historical datum is basically a kvp, where the Key (ComponentType) is an enum.
public class DetailedCurrencyResponse : DistinctiveCurrencyResponse
{
public Dictionary<ComponentType, List<ComponentHistoricalDatum>> Historical { get; set; }
}
public class ComponentHistoricalDatum
{
public DateTime CreatedAt { get; set; }
public string Value { get; set; }
}
The query you have outlined will attempt to return you a single Currency object, but given you are looking for any with a given abbreviation, if multiple currency objects share an abbreviation, the SingleOrDefault could error due to multiple returns.
It sounds like you want to define a structure to represent the currency pairs. That structure is not a Currency entity, but a different data representation. These are commonly referred to as ViewModels or DTOs. Once you've defined what you want to return, you can use .Select() to populate that from the Currency and applicable abbreviations.
For instance, if I create a CurrencySummaryDto which will have the currency ID, Abbrevation, and a string containing all of the applicable pairs:
public class CurrencySummaryDto
{
public long CurrencyId { get; set; }
public string Abbreviation { get; set; }
public string Pairs { get; set;}
}
... then the query...
var currencySummary = _unitOfWork.GetRepository<Currency>()
.GetQueryable()
.AsNoTracking()
.Where(c => c.Abbrv.Equals(abbreviation, StringComparison.InvariantCultureIgnoreCase)
&& c.DeletedAt == null && c.IsEnabled)
.Select( c => new {
c.Id,
c.Abbrv,
Pairs = c.PartialCurrencyPairs.Select(pc => pc.PairName).ToList() // Get names of pairs, or select another annonymous type for multiple properties you care about...
}).ToList() // Alternatively, when intending for returning lots of data use Skip/Take for paginating or limiting resulting data.
.Select( c => new CurrencySummaryDto
{
CurrencyId = c.Id,
Abbreviation = c.Abbrv,
Pairs = string.Join(", ", c.Pairs)
}).SingleOrDefault();
This is if you want to do something like combine data from the currency pairs into something like a string. If you're happy to leave them as a collection of simplified data, then the extra anonymous type and .ToList() are not required, just select directly into the Dto structure. This example combines the data into a string where string.Join() is not supported in EF expressions so we have to get our data out into objects to hand over to Linq2Object for the final mapping.
Edit: Ok, you're requirement/example just got a whole lot more complicated with the pair structure, but you should be able to leverage this into the query rather than selecting the entire graph of entities by moving the selection of those values up into the main query... However...
Given the complexity of the data relationships, the approach I would recommend using since this would be assumed to be a read-only result, would be to construct a View in the database to flatten these averages and totals, then bind a simplified entity to this view rather than attempting to manage this with EF Linq. I believe it can be done with linq, but it will be quite onerous to look at, and a view-based summary entity would be a lot cleaner while keeping the execution of this logic to be executed in the database.
Related
I have a class that has list of strings as a property:
public class FiltersDto
{
public int? MinPrice { get; set; }
public int? MaxPrice { get; set; }
public int? BathroomCount { get; set; }
public int? BedroomCount { get; set; }
public string Amenities { get; set; }
public List<string> Neighborhoods { get; set; }
}
In this method I try to check if the word "Neighborhood" is in the list
var sales = await
_propertyRepository
.GetAll()
.Include(x => x.PropertyType)
.WhereIf(input.Neighborhoods != null, x => x.Neighborhood.Contains(???????))
.WhereIf(input.MinPrice.HasValue && input.MaxPrice.HasValue, x => input.MinPrice <= x.Price && x.Price <= input.MaxPrice)
.WhereIf(input.BedroomCount.HasValue, x => x.BedroomCount == input.BedroomCount)
.WhereIf(input.BathroomCount.HasValue, x => x.BathroomCount == input.BathroomCount)
.Where(x => x.PropertyTypeId == 1)
.ToListAsync();
I need to check it in this row .WhereIf(input.Neighborhoods != null, x => x.Neighborhood.Contains(?))
How I can do this?
Hard to tell exactly form what is described, but if you have an instance of FiltersDto called myFiltersDto and it has a .Neighborhoods list like is like { "Red", "Blue", "Green"} and your x's .Neighborhood string is "Blah Red Sand"
x => myFiltersDto.Neighbourhoods.Any(n => x.Neighborhood.Contains(n))
i.e. you're asking "do any of these Neighborhoods n appear within the string held in x.Neighborhoods"
If you don't intend for the Contains to be a within-string string search, and e.g. x.Neighborhood will merely be "Red" and you're seeking an exact match in the collection of strings represented by Neighborhoods then it's
x => myFiltersDto.Neighbourhoods.Contains(x.Neighborhood))
I'd recommend to try and avoid using the word "Contains" when describing this, because it generates confusion between string.Contains (substring search) and list.Contains (element search)
Given the following C# code below, I am trying to retrieve all the continents having provinces whose cities include:
a city with Name and Address respectively set to "Aloma" and "123" and
another city with Name and Address respectively set to "Hemane" and "435".
public class Continent
{
public string Name { get; set; }
public List<Country> Countries{ get; set; }
}
public class Countries
{
public string Name { get; set; }
public List<Province> Provinces{ get; set; }
}
public class Province
{
public string Name { get; set; }
public List<Province> Cities { get; set; }
}
public class City
{
public string Name { get; set; }
public string Address { get; set; }
}
I have tried to use the query below, but it does not seem to work. Can you please help me?
Expression<Func<Province, bool>> matchCities = rec =>
(rec.Cities.Count(fi => fi.Name == "Aloma" && fi.Address== "123") > 0)
&& (rec.Cities.Count(fj => fj.Name == "Hemane" && fj.Address== "435") > 0);
Func<Province, bool> funcMatchCities= matchCities.Compile();
var results3 = session.Query<Continent>()
.Where(top => top.Countries.Any(ta => ta.Province.Any(
rec => funcMatchCities(rec))))
.OfType<Continent>()
.ToList();
You can query it like so:
var results3 = session.Query<Continent>()
.Where(top => top.Countries.Any(ta => ta.Province.Any(
rec =>
(rec.Fields.Any(fi => fi.Name == "Aloma" && fi.Address== "123") )
&& (rec.Fields.Any(fj => fj.Name == "Hemane" && fj.Address== "435") )))
.OfType<Continent>()
.ToList();
Note that you cannot Compile expression when you send them to the linq provider.
I'm a bit lost here and I've tried a few different ways to tackle it. So far I'm having a hard time writing out the LINQ to do what I want.
I want to take the user input string which can be multiple keywords split either by whitespace or ",".
This here works grabs the whole search term and compares it to the title in the Post or any tag I may have. I want the user to type in "HTML Preview" which would match a post called, "Preview the World" with the tags "HTML", "CSS", etc....
This query won't work...but I'm trying to modify it so that it does work.
public IPagedList<Post> SearchResultList(string searchTerm, int resultsPerPage, int page)
{
string[] terms = searchTerm.Split(null);
TNDbContext context = DataContext;
return context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.Where(c => (c.Title.Contains(searchTerm) || c.Tags.Any(d => d.Name.StartsWith(searchTerm))) || searchTerm == null)
.OrderByDescending(x => x.Views)
.ToPagedList(page, resultsPerPage);
}
I tried writing this instead of the other "Where" statement
.Where(x => (terms.All(y => x.Title.Contains(y))) || terms == null)
but it keeps throwing this error
Cannot compare elements of type 'System.String[]'. Only primitive types, enumeration types and entity types are supported.
FOR REFERENCE:
public class Post
{
public Post()
{
Tags = new HashSet<Tag>();
Comments = new HashSet<Comment>();
}
public int Id { get; set; }
public string Title { get; set; }
public string UrlTitle { get; set; }
public DateTime Date { get; set; }
public DateTime DateEdited { get; set; }
public string Body { get; set; }
public string Preview { get; set; }
public string PhotoPath { get; set; }
public int Views { get; set; }
//Navigational
public ICollection<Tag> Tags { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Tag
{
public Tag()
{
Post = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; }
public int TimesTagWasUsed { get; set; }
//Navigational
public ICollection<Post> Post { get; set; }
}
You need to start with a base query, and then keep adding where clauses to it for each search term. Try this:
TNDbContext context = DataContext;
//Create the base query:
var query = context.Posts
.Include(a => a.Tags)
.Include(b => b.Comments)
.OrderByDescending(x => x.Views);
//Refine this query by adding "where" filters for each search term:
if(!string.IsNullOrWhitespace(searchTerm))
{
string[] terms = searchTerm.Split(" ,".ToCharArray(),
StringSplitOptions.RemoveEmptyEntries);
foreach(var x in terms)
{
string term = x;
query = query.Where(post => (post.Title.Contains(term) ||
post.Tags.Any(tag => tag.Name.StartsWith(term))));
}
}
//Run the final query to get some results:
var result = query.ToPagedList(page, resultsPerPage);
return result;
You can nest queries with additional 'from' statements, so something like this should work:
var list = (from post in context.Posts.Include(a => a.Tags).Include(b => b.Comments)
from term in terms
where post.Title.Contains(term) || post.Tags.Any(d => d.Name.StartsWith(term))
select post).OrderByDescending(x => x.Views);
First of all, I am new to Entity Framework etc and trying to figure some things out. I have a model like this:
public class EventInstance {
[Column("EVENT_INSTANCE_ID")]
public int EventInstanceID { get; set; }
[Column("CUSTOMER_ID")]
public int CustomerID { get; set; }
[Column("EVENT_ID")]
public int EventID { get; set; }
[Column("START_DATE_TIME")]
public System.DateTime StartDateTime { get; set; }
public virtual Event Event { get; set; }
}
I need to access a property in a table called EventTimeEventInstances but this table is not included in the model. I have two questions.
If I add:
public virtual ICollection<EventTimeEventInstance> EventTimeInstances { get; set; }
Will that effect other areas of our application?
Secondly, how do I access the property from the ICollection in a query like this:
public IQueryable<EventInstance> GetInstances(int scheduleID) {
// only returning instances that are 3 months back
DateTime dateRange = DateTime.Now.AddDays(-180);
return EventDBContext.EventInstances.Where
(x => x.CustomerID == MultiTenantID && x.StartDateTime >= dateRange)
.OrderBy(x => x.StartDateTime).AsQueryable();
}
I need to be able to add EventTimeInstances.EventTimeID == scheudleID to this query. How can I do this?
You can use it like that in your query:
public IQueryable<EventInstance> GetInstances(int scheduleID)
{
// only returning instances that are 3 months back
DateTime dateRange = DateTime.Now.AddDays(-180);
return EventDBContext.EventInstances.Where(x =>
x.CustomerID == MultiTenantID &&
x.StartDateTime >= dateRange &&
x.EventTimeInstances.Any(a => a.EventTimeID == scheudleID) ).OrderBy(x => x.StartDateTime).AsQueryable();
}
Below are my classes
public class Student {
public long Id { get; set; }
public long? CollegeId { get; set; }
public StudentPersonal StudentPersonal { get; set; }
}
public class StudentPersonal {
public long? EthnicityId { get; set; }
public bool? GenderId { get; set; } // doesn't exist on UpdateModel, requires PropertyMap.SourceMember null check
}
public class UpdateModel{
public long Id { get; set; }
public long? CollegeId { get; set; }
public long? StudentPersonalEthnicityId { get; set; }
}
Below is the AutoMapper config
Mapper.Initialize(a => {
a.RecognizePrefixes("StudentPersonal");
}
Mapper.CreateMap<UpdateModel, StudentPersonal>()
.ForAllMembers(opt => opt.Condition(src => src.PropertyMap.SourceMember != null && src.SourceValue != null));
Mapper.CreateMap<UpdateModel, Student>()
.ForMember(dest => dest.StudentPersonal, opt => opt.MapFrom(src => Mapper.Map<StudentPersonal>(src)))
.ForAllMembers(opt => opt.Condition(src => src.PropertyMap.SourceMember != null && src.SourceValue != null));
And the sample test case:
var updateModel = new StudentSignupUpdateModel();
updateModel.Id = 123;
updateModel.CollegeId = 456;
updateModel.StudentPersonalEthnicityId = 5;
var existingStudent = new Student();
existingStudent.Id = 123;
existingStudent.CollegeId = 777; // this gets updated
existingStudent.StudentPersonal = new StudentPersonal();
existingStudent.StudentPersonal.EthnicityId = null; // this stays null but shouldn't
Mapper.Map(updateModel, existingStudent);
Assert.AreEqual(777, existingStudent.CollegeId); // passes
Assert.AreEqual(5, existingStudent.StudentPersonal.EthnicityId); // does not pass
Has anyone gotten conditional mapping to work with prefixes? It works ok on the non prefixed object.
The lambda you're passing to opts.Condition is too restrictive:
src => src.PropertyMap.SourceMember != null && src.SourceValue != null
In this property's case, src.PropertyMap is null every time (which you might expect, since there's no single property that maps to the destination nested property from the source object).
If you remove the PropertyMap.SourceMember check, your tests will pass. I'm not sure what impact this will have on the rest of your mapping though.