I'm new to ASP.NET and MVC so please bear with me. I have two models:
public class Change
{
public int ID { get; set; }
public string Title { get; set; }
// more fields
public virtual ICollection<ChangeHistory> ChangeHistory { get; set; }
}
and
public class ChangeHistory
{
public int ID { get; set; }
public int ChangeID { get; set; }
public DateTime Date {get; set; }
public Status Status {get; set; }
}
Currently, my Changes view simply lists all changes in a table with their details (ID, Title etc.). What I would like is to add several columns from the details table such as Inititiaton Date (Date of the first ChangeHistory item for a given Change) and Current Status & Date (both retrieved from the last ChangeHistory item for a given Change.)
I started by creating a new ViewModel:
public class ChangeViewModel
{
// Take these details from the change record
public int ID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
// Take these details from the first change history record
public DateTime InitiationDate { get; set; }
// Take these details from the latest change history record
public DateTime StatusDate { get; set; }
public Status CurrentStatus { get; set; }
}
In my ChangesController I got as far as retrieving the Initiation Date before my lack of experience stopped me in my tracks:
var ChangeWithStatus = from c in db.Changes
from h in db.ChangeHistories
where c.ID == h.ChangeID && h.Date ==
(
from h2 in db.ChangeHistories
where c.ID == h2.ChangeID
select h2.Date
).Min()
select new ChangeViewModel
{
// Change details
ID = c.ID,
Title = c.Title,
Description = c.Description,
// Initiation details
InitiationDate = h.Date,
Initiator = h.User,
// Status details
StatusDate = h.Date,
CurrentStatus = h.Status,
User = h.User
};
I'm wondering if the solution is to retrieve each ChangeHistory record with its own linq statement and then combine them with a third. Can anyone help me with this problem? Thanks in advance.
This should work:
var changeViewModels = (from change in db.Changes
let lastChangeHistory = change.ChangeHistory.OrderByDescending(changeHistory => changeHistory.Date).FirstOrDefault()
let firstChangeHistory = change.ChangeHistory.OrderBy(changeHistory => changeHistory.Date).FirstOrDefault()
select new ChangeViewModel
{
ID = change.ID,
Title = change.Title,
Description = change.Description,
InitiationDate = firstChangeHistory.Date,
StatusDate = lastChangeHistory.Date,
CurrentStatus = lastChangeHistory.Status,
}).ToList();
Using let clause this line:
let lastChangeHistory = change.ChangeHistory.OrderByDescending(changeHistory => changeHistory.Date).FirstOrDefault()
will give you the last change history for every change.
while this line:
let firstChangeHistory = change.ChangeHistory.OrderBy(changeHistory => changeHistory.Date).FirstOrDefault()
will give you the first.
Now you have all the data you need and you can select it into ChangeViewModel.
Related
I am having some issues trying to figure out how to display the names of the state and city instead of the StateId and CityId.
I am aware that you can put an instance of States and Cities in the model like this:
public virtual Cities Cities { get; set; }
public virtual States States { get; set; }
And then add this to the view:
#Html.DisplayFor(modelItem => item.Cities.CityName)
#Html.DisplayFor(modelItem => item.States.StateName)
However I have a few things that do not allow me to do this without error.
I use a ViewModel that gives me what I need for the view. It relies on a list from Addresses Table and in the Addresses Model I have Select Lists for these so that I can populate a dropdown list for each on edit and create.
When I add these to this to the Model and remove the selectLists I get an error that says it cannot find the Column Cities_CityId, States_StateId.
When I add it to the viewModel it just isn't available on the view. If I only needed it while showing 1 record I can use
Model.Cities.CityName
But I am retrieving several records, if they exist and using that would display the same city and state for all records, I believe..
I believe that all of my joins are correct but could be missing something.
Below is the ViewModel:
public partial class AddressOverview
{
public static AddressOverview GetAddressByCompany(int id, GeneralEntities db)
{
var qCus = from ad in db.Addresses
join cn in db.CompanyNames on ad.CompanyId equals cn.CompanyId
join cit in db.Cities on ad.City equals cit.CityId
join st in db.States on ad.State equals st.StateId
where (ad.CompanyId == id)
select new AddressOverview()
{
AddressId = ad.AddressId,
Customer = cn.CompanyName,
Location = ad.LocationName,
Addresses = ad,
CompanyId = ad.CompanyId,
State = st.StateName,
City = cit.CityName
};
var result = qCus.FirstOrDefault();
if (result != null)
{
result.AddressDetail = db.Addresses.Where(a => a.CompanyId == result.CompanyId);
};
return result;
}
public int AddressId { get; set; }
public string Customer { get; set; }
public string Location { get; set; }
public int CompanyId { get; set; }
public string City { get; set; }
public string State { get; set; }
public virtual Addresses Addresses { get; set; }
public virtual CompanyNames CompanyName { get; set; }
public virtual Cities Cities { get; set; }
public virtual States States { get; set; }
public virtual IEnumerable<Addresses> AddressDetail { get; set; }
}
And here is the controller:
public ActionResult Index(int id)
{
//Trying to get a view model for customer from the received UserId.
AddressOverview modelInstance = AddressOverview.GetAddressByCompany(id, db);
if (modelInstance == null)
{
//return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
return RedirectToAction("/Add/", new { id });
}
return View(modelInstance);
}
I am wondering if maybe there is a way to do this in the controller using the modelInstance? Please forgive me, I am trying to clean up some things that someone else did and my skills are a little weak.
If you need to see something else let me know.
Thanks for your help!
Update:
I just had a smack in the head moment.. Looking at View from question posted below I noticed that I was calling it from the Addresses Model instead of the ViewModel. So I corrected that but it still does not work..
Changed from this:
#model MyProject.GenerDTO.Entities.Addresses
To:
#model MyProject.Models.AddressOverview
Update:
Here is the solution I came up with. Probably not the best, but certainly a quick fix..
I decided to create a view on the MSSQL side and bring all of this together and then used that in the view and the ViewModel. If you decide to do something like this. Be sure to make sure that it is [NotMapped] in the entities. We are only using this to view and it is not meant to be used for editing, creating, etc.
Here is the new View Model:
public partial class AddressOverview
{
public static AddressOverview GetAddressByCompany(int id, GeneralEntities db)
{
var qCus = from ad in db.AddressView
join cn in db.CompanyNames on ad.CompanyId equals cn.CompanyId
where (ad.CompanyId == id)
select new AddressOverview()
{
AddressId = ad.AddressId,
Customer = cn.CompanyName,
Location = ad.LocationName,
AddressView = ad,
CompanyId = ad.CompanyId
};
var result = qCus.FirstOrDefault();
if (result != null)
{
result.AddressDetail = db.AddressView.Where(a => a.CompanyId == result.CompanyId);
};
return result;
}
public int AddressId { get; set; }
public string Customer { get; set; }
public string Location { get; set; }
public int CompanyId { get; set; }
public virtual AddressView AddressView { get; set; }
public virtual CompanyNames CompanyName { get; set; }
public virtual IEnumerable<AddressView> AddressDetail { get; set; }
}
I believe that creating a view in SQL is clean and the query is done server side. It can be treated like a table in the project. I am open to alternatives.
I have the following:
void Main()
{
// I need to get the below query, but with the newest comment date as well of each post.
// Gives me all the posts by the member
var member_id = 1139;
var query = (from posts in Ctm_Forum_Posts
join category in Ctm_Forum_Categories on posts.FK_Categori_ID equals category.Id
where posts.Archieved == false
&& posts.Deleted == false
&& posts.FK_Member_ID == member_id
select new ForumPostModel(){
Id = posts.Id,
BodyText = posts.BodyText,
Summary = posts.Summary,
Title = posts.Title,
Created = posts.Created,
Updated = posts.Updated,
CategoryName = category.Title,
});
// this gives the newest comment date (registration_timestamp) from a specific post with id = 1
var NewestCommentDate = (from comments in Ctm_Comments
join posts in Ctm_Forum_Posts on comments.Page_ID equals posts.Id
where posts.Id == 1 && comments.Deleted == false
orderby comments.Reqistration_timestamp descending
select comments.Reqistration_timestamp).FirstOrDefault();
}
// Model of the result.
public class ForumPostModel{
public int Id { get; set; }
public string Title { get; set; }
public string BodyText { get; set; }
public string Summary { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
public string CategoryName { get; set; }
public DateTime LatestCommentTime { get; set; }
}
Right now the two queries work, but my problem is the performance sucks,, so im trying to combine the results into a single query, so my load time will be much less.
I tried searching about unions, but have been unable to figure it out.
Not sure if i worded the question correctly, but what im trying to do is return a new viewmodel with one of the parts being a booking:
public class Booking
{
public int BookingId { get; set; }
public int CustomerId { get; set; }
public Guid UniqueId { get; set; }
public string EventId { get; set; }
public bool IsPaid { get; set; }
public double Price { get; set; }
public DateTime BookingDate { get; set; }
public DateTime DateBooked { get; set; }
[JsonIgnore]
public Customer Customer { get; set; }
[JsonIgnore]
public ICollection<BookingService> BookingServices { get; set; }
[NotMapped]
public IEnumerable<Service> Services { get; set; }
}
and my query is:
var customers = _dbContext.Customers
.Select(c => new CustomerBookingsViewModel
{
Customer = c,
Bookings = c.Bookings.Select(b => new Booking
{
BookingId = b.BookingId,
BookingDate = b.BookingDate,
DateBooked = b.DateBooked,
CustomerId = b.CustomerId,
UniqueId = b.UniqueId,
EventId = b.EventId,
IsPaid = b.IsPaid,
Price = b.Price,
Services = b.BookingServices.Select(s => s.Service)
}),
}
)
.ToList();
What I want to know is how to I select all the booking info into the booking without selecting each part, ie:
BookingId = b.BookingId,
BookingDate = b.BookingDate,
DateBooked = b.DateBooked,
CustomerId = b.CustomerId,
UniqueId = b.UniqueId,
EventId = b.EventId,
IsPaid = b.IsPaid,
Price = b.Price,
Can it be done or because the list of services is inside the booking model it cant?
Thanks.
You could implement the IClonable interface on your class.
public class MyClass : ICloneable
{
public int Id { get; set; }
public object Clone() => MemberwiseClone();
}
Usage:
var list1 = new List<MyClass>
{
new MyClass() { Id = 2 },
new MyClass() { Id = 5 }
};
var list2 = list1.Select(x => (MyClass)x.Clone()).ToList();
list2.First().Id = 10; //list1 won't be affected
You should use AutoMapper here to avoid writing each path.
https://automapper.org/
http://docs.automapper.org/en/stable/Getting-started.html
There is no other way, at least it is not related to LINQ or queries.
The question "How to clone an object" has been answered here:
Creating a copy of an object in C#
There is no LINQ way to do this. I would suggest using custom Attribute marking every property you want to copy. This would help if you want not to copy the whole object but some properties. After marking every property you need you can just set the marked props with reflection from one of the objects to the other.
I am trying to get the average rating of all restaurants and return the names of all resteraunts associated with that id, I was able to write a sql statement to get the average of restaurants along with the names of the restaurants however I want to only return the name of the restaurant once.
Select t.Average, Name from [dbo].[Reviews] as rev
join [dbo].[Resteraunts] as rest
on rest.ResterauntId = rev.ResterauntId
inner join
(
SELECT [ResterauntId],
Avg(Rating) AS Average
FROM [dbo].[Reviews]
GROUP BY [ResterauntId]
)
as t on t.ResterauntId = rest.ResterauntId
resteraunt class
public int ResterauntId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
public virtual Review reviews{ get; set; }
Review class
public int ReviewId { get; set; }
public double Rating { get; set; }
[ForeignKey("ResterauntId")]
Resteraunt Resteraunt { get; set; }
public int ResterauntId { get; set; }
public DateTime DateOfReview { get; set; }
If possible I would like to have the answer converted to linq.
Resteurants.Select(r => new {
Average = r.Reviews.Average(rev => rev.Rating),
r.Name
})
This should give you a set of objects that have Average (the average of all reviews for that restaurant) and the Name of the restaurant.
This assumes that you have correctly setup the relationships so that Restaurant.Reviews only refers to the ones that match by ID.
If you don't have that relationship setup and you need to filter it yourself:
Resteurants.Select(r => new {
Average = Reviews.Where(rev => rev.ResteurantId == r.Id).Average(rev => rev.Rating),
r.Name
})
Firstly your models seems to have more aggregation than required, I have taken the liberty to trim it and remove extra fields, ideally all that you need a Relation ship between two models RestaurantId (Primary Key for Restaurant and Foreign Key (1:N) for Review)
public class Restaurant
{
public int RestaurantId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
public class Review
{
public int ReviewId { get; set; }
public double Rating { get; set; }
public int RestaurantId { get; set; }
public DateTime DateOfReview { get; set; }
}
If these are the models, then you just need List<Restaurant> restaurantList, since that internally contains the review collection, then all that you need is:
var result =
restaurantList.Select(x => new {
Name = x.Name,
Average = x.Reviews.Average(y => y.Rating)
}
);
In case collection aggregation is not there and you have separate ReviewList as follows: List<Review> reviewList, then do the following:
var result =
reviewList.GroupBy(x => x.RestaurantId, x => new {x.RestaurantId,x.Rating})
.Join(restaurantList, x => x.Key,y => y.RestaurantId,(x,y) => new {
Name = y.Name,
AvgRating = x.Average(s => s.Rating)
});
Also please note this will only List the Restaurants, which have atleast one review, since we are using InnerJoin, otherwise you need LeftOuterJoin using GroupJoin, for Restaurants with 0 Rating
I see your Restaurant class already has an ICollection<Review> that represents the reviews of the restaurant. This is probably made possible because you use Entity Framework or something similar.
Having such a collection makes the use of a join unnecessary:
var averageRestaurantReview = Restaurants
.Select(restaurant => new
.Where(restaurant => ....) // only if you don't want all restaurants
{
Name = restaurant.Name,
AverageReview = restaurants.Reviews
.Select(review => review.Rating)
.Average(),
});
Entity Framework will do the proper joins for you.
If you really want to use something join-like you'd need Enumerable.GroupJoin
var averageRestaurantReview = Restaurants
.GroupJoin(Reviews, // GroupJoin Restaurants and Reviews
restaurant => restaurant.Id, // From every restaurant take the Id
review => review.RestaurantId, // From every Review take the RestaurantId
.Select( (restaurant, reviews) => new // take the restaurant with all matching reviews
.Where(restaurant => ....) // only if you don't want all restaurants
{ // the rest is as before
Name = restaurant.Name,
AverageReview = reviews
.Select(review => review.Rating)
.Average(),
});
I'm trying to use a calculated value in my OrderBy clause in a LINQ query.
The error I am getting is:
DbArithmeticExpression arguments must have a numeric common type.
My model looks like this:
public class PostModel
{
public int ID { get; set; }
public DateTime Created { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string FilePath { get; set; }
public float Rank { get; set; }
public UserProfile Creator { get; set; }
public bool ShowPost { get; set; }
public PostModel()
{
Created = DateTime.Now;
Rank = 0;
ShowPost = false;
}
}
and I'm trying to select posts using this:
var todaysDate = DateTime.Now.AddDays(-10);
var result = _database.Posts
.Where(p => p.ShowPost == true)
.OrderBy(x => ((float)x.Rank) - (((float)(x.Created - todaysDate).TotalDays)) / 2f)
.Skip(page * StaticVariables.ResponseDataPageSize)
.Take(StaticVariables.ResponseDataPageSize)
.Select(s => new
{
id = s.ID,
rank = s.Rank,
title = s.Title,
description = s.Description
}
);
It's the order by causing the error. I first thought it was that I was not casting all my variables to the same type, but adding (float) does not seem to help.
The purpose of the code is to make make high ranking posts fall down the list over time as to allow newer information to be shown.
Any ideas?
Use EntityFunctions in LinqToEntity:
EntityFunctions.DiffDays(todaysDate, x.Created)