I have been following the book Pro ASP.net MVC 2 Framework, which I have found to be quite brilliant. But it's a real learning curve and now I'm stuck.
In the book you build something like the below, which allows for paging.
public ViewResult List([DefaultValue(0)] string cityzip, [DefaultValue(1)] int page)
{
var roomsToShow = roomsRepository.Rooms.Where(x => x.CountryID == cityzip);
var viewModel = new RoomsListViewModel
{
Rooms = roomsToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = roomsToShow.Count()
}
};
return View(viewModel); // Passed to view as ViewData.Model (or simply Model)
}
I think needed to adapt this, so that I could do a join on the search
public ViewResult List([DefaultValue(0)] string cityzip, [DefaultValue(1)] int page)
{
var roomsToShow = roomsRepository.Rooms.Join(
roomCoordinatesRepository.RoomCoordinates,
room => room.RoomID,
roomCoordinate => roomCoordinate.RoomID,
(room, roomCoordinate) => new { RoomCoordinate = roomCoordinate, Room = room });
var viewModel = new RoomsListViewModel
{
Rooms = roomsToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = roomsToShow.Count()
}
};
return View(viewModel); // Passed to view as ViewData.Model (or simply Model)
}
...but immediately I get an intellisense error saying -
Cannot implicitly convert type 'System.Collections.Generic.List<AnonymousType#1>' to 'System.Collections.Generic.IList<MeetingRoom.Domain.Entities.Room>'. An explicit conversion exists (are you missing a cast?)
I obviously don't understand the code well enough to figure out what is wrong. I'm also feeling a bit out of my depth with this lamda linq stuff
Room is a domain object which is defined as:
namespace MeetingRoom.Domain.Entities
{
[Table(Name = "Rooms")]
public class Room
{
[Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert)]
public int RoomID { get; set; }
[Column] public string Name { get; set; }
[Column] public string Description { get; set; }
[Column] public decimal Price { get; set; }
[Column] public string Category { get; set; }
[Column] public string Pcode { get; set; }
[Column] public int CountryID { get; set; }
public MeetingRooms.Domain.entities.RoomCoordinate RoomCoordinate { get; set; }
}
}
and represents my Room table. Do I need some sort of parent entity that would represent the join between the Room and Room-co-ordinates table?
The co-ordinates entity looks like this:
namespace MeetingRooms.Domain.entities
{
[Table(Name = "RoomCoordinate")]
public class RoomCoordinate
{
[Column(IsPrimaryKey = true, IsDbGenerated = true, AutoSync = AutoSync.OnInsert, Name = "ID")]
public int CoordID { get; set; }
[Column]
public int RoomID { get; set; }
[Column]
public string Coordinates { get; set; }
[Column]
public decimal Latitude { get; set; }
[Column]
public decimal Longitude { get; set; }
}
}
The RoomsListViewModel looks like follows:
namespace MeetingRoomsMVC.WebUI.Models
{
public class RoomsListViewModel
{
public IList RoomsWithCoordinates { get; set; }
public PagingInfo PagingInfo { get; set; }
}
}
The problem is, in this code
var roomsToShow = roomsRepository.Rooms.Join(
roomCoordinatesRepository.RoomCoordinates,
room => room.RoomID,
roomCoordinate => roomCoordinate.RoomID,
(room, roomCoordinate) => new { RoomCoordinate = roomCoordinate, Room = room });
you're constructing an IEnumerable of anonymous-type objects: (room, roomCoordinate) => new { RoomCoordinate = roomCoordinate, Room = room }
And then, in the next line you're trying to assing it to a list of Room.
The problem can be resolved by initially creating an IEnumerable of the correct item type:
var roomsToShow = roomsRepository.Rooms.Join(
roomCoordinatesRepository.RoomCoordinates,
room => room.RoomID,
roomCoordinate => roomCoordinate.RoomID,
(room, roomCoordinate) => new MeetingRoom.Domain.Entities.Room{ RoomCoordinate = roomCoordinate, Room = room });
(note the class name in the lambda).
Here's my suggestion based on the OP's further description:
1) Create an aggregate class that holds both Room and RoomCoordinates info:
public class RoomWithCoordinates
{
public Room Room { get; set; }
public RoomCoordinates Coordinates { get; set; }
}
2) Modify your controller action as follows:
public ViewResult List([DefaultValue(0)] string cityzip, [DefaultValue(1)] int page)
{
var roomsToShow = roomsRepository.Rooms.Join(
roomCoordinatesRepository.RoomCoordinates,
room => room.RoomID,
roomCoordinate => roomCoordinate.RoomID,
(room, roomCoordinate) => new RoomWithCoordinates{ Coordinates = roomCoordinate, Room = room } );
var viewModel = new RoomsListViewModel
{
RoomsWithCoordinates = roomsToShow.Skip((page - 1) * PageSize).Take(PageSize).ToList(),
PagingInfo = new PagingInfo
{
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = roomsToShow.Count()
}
};
return View(viewModel); // Passed to view as ViewData.Model (or simply Model)
}
3) Modify your RoomsListViewModel class and your view to reflect these changes.
Related
I have a Web API for OData services. I have a lot of table with many relations. Here is some of the table:
MSADDRESSCOUNTRY
public partial class MSADDRESSCOUNTRY
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSCOUNTRY()
{
this.MSADDRESSPROVINCEs = new HashSet<MSADDRESSPROVINCE>();
}
public int ID { get; set; }
public string CODE { get; set; }
public string COUNTRYNAME { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSPROVINCE> MSADDRESSPROVINCEs { get; set; }
}
MSADDRESSPROVINCE
public partial class MSADDRESSPROVINCE
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSPROVINCE()
{
this.MSADDRESSDISTRICTs = new HashSet<MSADDRESSDISTRICT>();
}
public int ID { get; set; }
public Nullable<int> COUNTRYID { get; set; }
public string PROVINCENAME { get; set; }
public virtual MSADDRESSCOUNTRY MSADDRESSCOUNTRY { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage","CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSDISTRICT> MSADDRESSDISTRICTs { get; set; }
}
MSADDRESSDISTRICT
public partial class MSADDRESSDISTRICT
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public MSADDRESSDISTRICT()
{
this.MSADDRESSSUBDISTRICTs = new HashSet<MSADDRESSSUBDISTRICT>();
}
public int ID { get; set; }
public Nullable<int> PROVINCEID { get; set; }
public string DISTRICTNAME { get; set; }
public virtual MSADDRESSPROVINCE MSADDRESSPROVINCE { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MSADDRESSSUBDISTRICT> MSADDRESSSUBDISTRICTs { get; set; }
}
I create DTO object model for every table with the property is the same with Database object model.
I want the client can use $expand keyword to get child data and/or parent data.
For MSADDRESSCOUNTRY I need to write the code like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressCountryObject> Get()
{
return db.MSADDRESSCOUNTRies.Select(c => new MsAddressCountryObject
{
ID = c.ID,
CODE = c.CODE,
COUNTRYNAME = c.COUNTRYNAME,
MSADDRESSPROVINCEs = c.MSADDRESSPROVINCEs.Select(data => new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = new MsAddressCountryObject()
{
ID = data.MSADDRESSCOUNTRY.ID,
CODE = data.MSADDRESSCOUNTRY.CODE,
COUNTRYNAME = data.MSADDRESSCOUNTRY.COUNTRYNAME,
},
MSADDRESSDISTRICTs = data.MSADDRESSDISTRICTs.Select(dist => new MsAddressDistrictObject()
{
ID = dist.ID,
PROVINCEID = dist.PROVINCEID,
DISTRICTNAME = dist.DISTRICTNAME,
})
})
});
}
For MSADDRESSPROVINCE I need to write the code like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressProvinceObject> Get()
{
return db.MSADDRESSPROVINCEs.Select(data => new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = new MsAddressCountryObject()
{
ID = data.MSADDRESSCOUNTRY.ID,
CODE = data.MSADDRESSCOUNTRY.CODE,
COUNTRYNAME = data.MSADDRESSCOUNTRY.COUNTRYNAME,
},
MSADDRESSDISTRICTs = data.MSADDRESSDISTRICTs.Select(dist => new MsAddressDistrictObject()
{
ID = dist.ID,
PROVINCEID = dist.PROVINCEID,
DISTRICTNAME = dist.DISTRICTNAME
})
});
}
That code works fast. But if I add/change/remove column, I have to modify the controller manually, one by one for all controller. For example, if I want to add geological coordinate in MSADDRESSDISTRICT, I have to change the code in Country Controller, Province Controller and District Controller.
So I decide to create extension method like this.
public static MsAddressCountryObject ToDTO(this MSADDRESSCOUNTRY data)
{
return new MsAddressCountryObject()
{
ID = data.ID,
CODE = data.CODE,
COUNTRYNAME = data.COUNTRYNAME,
};
}
public static IQueryable<MsAddressCountryObject ToDTO(this IEnumerable<MSADDRESSCOUNTRY datas)
{
return datas.Select(country =
{
var obj = country?.ToDTO();
obj.MSADDRESSPROVINCEs = country.MSADDRESSPROVINCEs?.ToDTO();
return obj;
}).AsQueryable();
}
public static MsAddressProvinceObject ToDTO(this MSADDRESSPROVINCE data)
{
return new MsAddressProvinceObject()
{
ID = data.ID,
COUNTRYID = data.COUNTRYID,
PROVINCENAME = data.PROVINCENAME,
MSADDRESSCOUNTRY = data.MSADDRESSCOUNTRY?.ToDTO()
};
}
public static IQueryable<MsAddressProvinceObject ToDTO(this IEnumerable<MSADDRESSPROVINCE datas)
{
return datas.Select(province =
{
var obj = province?.ToDTO();
obj.MSADDRESSDISTRICTs = province.MSADDRESSDISTRICTs.ToDTO();
return obj;
}).AsQueryable();
}
public static MsAddressDistrictObject ToDTO(this MSADDRESSDISTRICT data)
{
return new MsAddressDistrictObject()
{
ID = data.ID,
PROVINCEID = data.PROVINCEID,
DISTRICTNAME = data.DISTRICTNAME,
MSADDRESSPROVINCE = data.MSADDRESSPROVINCE?.ToDTO()
};
}
public static IQueryable<MsAddressDistrictObject ToDTO(this IEnumerable<MSADDRESSDISTRICT datas)
{
return datas.Select(district =
{
var obj = district?.ToDTO();
obj.MSADDRESSSUBDISTRICTs = district.MSADDRESSSUBDISTRICTs?.ToDTO();
return obj;
}).AsQueryable();
}
And the controller just like this.
[EnableQuery(MaxExpansionDepth = 4)]
public IQueryable<MsAddressCountryObject Get()
{
return db.MSADDRESSCOUNTRies.ToDTO()
}
And that makes the performance really bad. I think the extension is making a lot of memory allocation or some thing that make the result not being delivered directly to the client.
My goal is to create the code easy to maintain, and the performance not drop significantly.
I have many relation in other table. I want the $expand works without write all parent/child Select statement manually and one by one.
I have try to not calling ToDTO() from all the extension method. The result is the performance is fast. But I lost all the relation or I need to write the parent/child Select statement for all method.
Any suggestion will help.
Thanks.
I`m having simple method which builds IQueryable and returns it.
public IQueryable<ClassDTO> ReportByNestedProperty()
{
IQueryable<Class> query = this.dbSet;
IQueryable<ClassDTO> groupedQuery =
from opportunity in query
group new
{
ItemGroup = opportunity.OpportunityStage.Name,
EstimatedRevenue = opportunity.EstimatedRevenue,
CostOfLead = opportunity.CostOfLead
}
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
into item
select new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Select(z => z.ItemGroup.Name).Count(), // int
Commission = item.Sum(z => z.EstimatedRevenue), // decimal
Cost = item.Sum(z => z.CostOfLead), // decimal?
};
return groupedQuery;
}
This is fine. The thing i need is to create method with same return type, but groupby by different prperties dynamically. So from the above code I want to have 3 dynamic parts which will be passed as params:
ItemGroup = opportunity.OpportunityStage.Name
and
by new
{
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
}
So the new method should be like this
public IQueryable<ClassDTO> ReportByNestedProperty(string firstNestedGroupByProperty, string secondNestedGroupByProperty)
{
// TODO: ExpressionTree
}
And call it like this:
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Id")
ReportByNestedProperty("OtherNestedProperty.Name","OtherNestedProperty.Id")
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Price")
So the main thing is to create expressions with these two selects:
opportunity.OpportunityStage.Name,
opportunity.OpportunityStage.Id
I have tried toe create the select expressions, groupby, the creation of Anonomoys classes and the DTO Class but I just cant get it right.
EDIT:
Here are the classes involved:
public class ClassDTO
{
public ClassDTO() { }
[Key]
public string ItemGroup { get; set; }
public decimal Commission { get; set; }
public decimal? Cost { get; set; }
public int Count { get; set; }
}
Class obj is a pretty big one so i`m posting just part of it
public partial class Class
{
public Class() { }
[Key]
public Guid Id { get; set; }
public Guid? OpportunityStageId { get; set; }
[ForeignKey(nameof(OpportunityStageId))]
[InverseProperty(nameof(Entities.OpportunityStage.Class))]
public virtual OpportunityStage OpportunityStage { get; set; }
}
public partial class OpportunityStage
{
public OpportunityStage()
{
this.Classes = new HashSet<Class>();
}
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
[InverseProperty(nameof(Class.OpportunityStage))]
public virtual ICollection<TruckingCompanyOpportunity> Classes{ get; set; }
}
I have simplified your Grouping query and introduced private class IdName which should replace anonymous class usage:
class IdName
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
static Expression MakePropPath(Expression objExpression, string path)
{
return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
}
IQueryable<ClassDTO> ReportByNestedProperty(IQueryable<Class> query, string nameProperty, string idProperty)
{
// Let compiler to do half of the work
Expression<Func<Class, string, int, IdName>> keySelectorTemplate = (opportunity, name, id) =>
new IdName { Name = name, Id = id };
var param = keySelectorTemplate.Parameters[0];
// generating expressions from prop path
var nameExpr = MakePropPath(param, nameProperty);
var idExpr = MakePropPath(param, idProperty);
var body = keySelectorTemplate.Body;
// substitute parameters
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[1], nameExpr, body);
body = ReplacingExpressionVisitor.Replace(keySelectorTemplate.Parameters[2], idExpr, body);
var keySelectorLambda = Expression.Lambda<Func<Class, IdName>>(body, param);
// finalize query
IQueryable<ClassDTO> groupedQuery = query
.GroupBy(keySelectorLambda)
.Select(item => new ClassDTO()
{
ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
Count = item.Count(x => x.Name), // int
Commission = item.Sum(x => x.EstimatedRevenue), // decimal
Cost = item.Sum(x => x.CostOfLead), // decimal?
});
return groupedQuery;
}
I've been trying to add an object to a list in my model, via the HTML form
My Recipe Class:
public long Id { get; set; }
private string _key;
public string Key
{
get
{
if (_key == null)
{
_key = Regex.Replace(Title.ToLower(), "[^a-z0-9]", "-");
}
return _key;
}
set { _key = value; }
}
[Required]
[Display(Name = "Ingredienser")]
public string Ingredients { get; set; }
[Required]
[Display(Name = "Fremgangsmåde")]
public string Method { get; set; }
[Required]
[Display(Name = "Titel")]
public string Title { get; set; }
public DateTime Posted { get; set; }
public List<Comment> Comments { get; set; } = new List<Comment>();
public List<Rating> Ratings { get; set; } = new List<Rating>();
public string Author { get; set; }
public Rating NewRating { get; set; } = new Rating();
public double CalculateRating()
{
if (Ratings.Count == 0)
return 0;
else
return Ratings.Average(x => x.Value);
}
What i've been trying:
#using (Html.BeginForm("Rating", "Home", FormMethod.Post, Model.Id))
{
<p>
#Html.LabelFor(x => x.NewRating)
#Html.RadioButtonFor(x => x.NewRating.Value, 0)
#Html.RadioButtonFor(x => x.NewRating.Value, 1)
#Html.RadioButtonFor(x => x.NewRating.Value, 2)
#Html.RadioButtonFor(x => x.NewRating.Value, 3)
</p>
<p>
<button type="submit">Giv Karakter</button>
</p>
}
and here's the method i'm posting to:
public IActionResult Rating([FromForm] int RecipeId, Rating rating)
{
rating.Posted = DateTime.Now;
var recipe = _db.Recipes.Where(x => x.Id == RecipeId).Include(e =>
e.Comments).FirstOrDefault();
recipe.Ratings.Add(rating);
_db.Update<Recipe>(recipe);
_db.SaveChanges();
return RedirectToAction("Index");
}
My problem is that i am not recieving a ratings object in my Rating method as i should, i can't figure out how to directly add an object to a list via a form
(this list is of course empty, since i cant add items to it yet)
Due to MVC binder behavior and deserialization, please keep the same instance name, during POST. So change the following line like below ("Rating NewRating")
public IActionResult Rating([FromForm] int RecipeId, Rating NewRating)
That should work.
I try to update my Products table but i can't because throw an error.
This is my hardcode controller:
[HttpPost]
public ActionResult EditProduct(ProductsViewModel productViewModel)
{
TechStoreEntities context = new TechStoreEntities();
Product newProduct = new Product
{
ProductId = productViewModel.ProductId,
Name = productViewModel.Name,
Price = productViewModel.Price,
Discount = productViewModel.Discount,
Quantity = productViewModel.Quantity,
Size = productViewModel.Size,
Description = productViewModel.Description,
ProducerName = productViewModel.ProducerName,
PaymentMethods = productViewModel.PaymentMethods,
CategoryID = productViewModel.CategoryID,
SubcategoryID = productViewModel.SubcategoryID,
IsNew = productViewModel.IsNew,
IsEnable = productViewModel.IsEnable
};
context.Entry(newProduct).State = EntityState.Modified;
context.SaveChanges();
ViewBag.CategoryID = new SelectList(context.Categories.Where(c => c.SubCategoryID == null), "CategoryID", "Name");
ViewBag.SubcategoryID = new SelectList(context.Categories.Where(c => c.SubCategoryID != null), "CategoryID", "Name");
return RedirectToAction("Products");
}
This is a model:
public class ProductsViewModel
{
public int ProductId { get; set; }
public string Name { get; set; }
public int Price { get; set; }
public int Discount { get; set; }
public int Quantity { get; set; }
public string Size { get; set; }
public string Description { get; set; }
public string ProducerName { get; set; }
public string PaymentMethods { get; set; }
public bool IsNew { get; set; }
public bool IsEnable { get; set; }
public string Category { get; set; }
public int CategoryID { get; set; }
public string Subcategory { get; set; }
public int SubcategoryID { get; set; }
public DateTime? CreateDate { get; set; }
}
I use strongly typed view:
#model MyTechStore.Models.ProductsViewModel
I add in a view:
#Html.HiddenFor(model => model.ProductId)
When i start app and enter some data to update existing data and press save, throw me exception:
System.Data.Entity.Infrastructure.DbUpdateConcurrencyException
When i debugging i saw that only the ProductId was 0. Everything else is OK. I tested with scaffolding controller but there is OK. I want to use view model, not as scaffolding controller use the model from my database.
Can someone tell me where i'm wrong?
My GET method:
public ActionResult EditProduct(int? id)
{
TechStoreEntities context = new TechStoreEntities();
ProductsManipulate product = new ProductsManipulate();
ProductsViewModel editProduct = product.EditProduct(id);
ViewBag.CategoryID = new SelectList(context.Categories.Where(c => c.SubCategoryID == null), "CategoryID", "Name");
ViewBag.SubcategoryID = new SelectList(context.Categories.Where(c => c.SubCategoryID != null), "CategoryID", "Name");
return View(editProduct);
}
And my data access layer:
public ProductsViewModel EditProduct(int? id)
{
TechStoreEntities context = new TechStoreEntities();
Product dbProduct = context.Products.Find(id);
ProductsViewModel product new ProductsViewModel
{
Name = dbProduct.Name,
Price = dbProduct.Price,
Quantity = dbProduct.Quantity,
CategoryID = dbProduct.CategoryID,
SubcategoryID = dbProduct.SubcategoryID,
IsNew = dbProduct.IsNew
};
return product;
}
You need to populate ProductId in ProductsViewModel
public ProductsViewModel EditProduct(int? id)
{
TechStoreEntities context = new TechStoreEntities();
Product dbProduct = context.Products.Find(id);
ProductsViewModel product = new ProductsViewModel()
{
// You need this line to pass the value to View
ProductId = dbProduct.ProductId,
Name = dbProduct.Name,
Price = dbProduct.Price,
Quantity = dbProduct.Quantity,
CategoryID = dbProduct.CategoryID,
SubcategoryID = dbProduct.SubcategoryID,
IsNew = dbProduct.IsNew
};
return product;
}
What that error is saying, is that EF tried to update a Product with those fields, but the it returned 0 RowCount therefore it knows something went wrong.
As you have mentioned before, the ProductId is 0, meaning you probably don't have a Product with that ID, and therefore when EF tries to update it, the row count is 0, which causes EF to throw a DbUpdateConcurrencyException.
You need to make sure your Id is populated if you want to an update an existing product.
Otherwise if you want an upsert (Update or Insert) you first need to check if a record exists for your given ProductId and if it does, do update, otherwise do insert.
I hope it's more clear what I want to do from the code than the title. Basically I am grouping by 2 fields and want to reduce the results into a collection all the ProductKey's constructed in the Map phase.
public class BlockResult
{
public Client.Names ClientName;
public string Block;
public IEnumerable<ProductKey> ProductKeys;
}
public Block()
{
Map = products =>
from product in products
where product.Details.Block != null
select new
{
product.ClientName,
product.Details.Block,
ProductKeys = new List<ProductKey>(new ProductKey[]{
new ProductKey{
Id = product.Id,
Url = product.Url
}
})
};
Reduce = results =>
from result in results
group result by new {result.ClientName, result.Block} into g
select new BlockResult
{
ClientName = g.Key.ClientName,
Block = g.Key.Block,
ProductKeys = g.SelectMany(x=> x.ProductKeys)
};
}
I get some weird System.InvalidOperationException and a source code dump where basically it is trying to initialize the list with an int (?).
If I try replacing the ProductKey with just IEnumerable ProductIds (and make appropriate changes in the code). Then the code runs but I don't get any results in the reduce.
You probably don't want to do this. Are you really going to need to query in this manner? If you know the context, then you should probably just do this:
var q = session.Query<Product>()
.Where(x => x.ClientName == "Joe" && x.Details.Block == "A");
But, to answer your original question, the following index will work:
public class Products_GroupedByClientNameAndBlock : AbstractIndexCreationTask<Product, Products_GroupedByClientNameAndBlock.Result>
{
public class Result
{
public string ClientName { get; set; }
public string Block { get; set; }
public IList<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public string Id { get; set; }
public string Url { get; set; }
}
public Products_GroupedByClientNameAndBlock()
{
Map = products =>
from product in products
where product.Details.Block != null
select new {
product.ClientName,
product.Details.Block,
ProductKeys = new[] { new { product.Id, product.Url } }
};
Reduce = results =>
from result in results
group result by new { result.ClientName, result.Block }
into g
select new {
g.Key.ClientName,
g.Key.Block,
ProductKeys = g.SelectMany(x => x.ProductKeys)
};
}
}
When replicating I get the same InvalidOperationException, stating that it doesn't understand the index definition (stack trace omitted for brevity).
Url: "/indexes/Keys/ByNameAndBlock"
System.InvalidOperationException: Could not understand query:
I'm still not entirely sure what you're attempting here, so this may not be quite what you're after, but I managed to get the following working. In short, Map/Reduce deals in anonymous objects, so strongly typing to your custom types makes no sense to Raven.
public class Keys_ByNameAndBlock : AbstractIndexCreationTask<Product, BlockResult>
{
public Keys_ByNameAndBlock()
{
Map = products =>
from product in products
where product.Block != null
select new
{
product.Name,
product.Block,
ProductIds = product.ProductKeys.Select(x => x.Id)
};
Reduce = results =>
from result in results
group result by new {result.Name, result.Block}
into g
select new
{
g.Key.Name,
g.Key.Block,
ProductIds = g.SelectMany(x => x.ProductIds)
};
}
}
public class Product
{
public Product()
{
ProductKeys = new List<ProductKey>();
}
public int ProductId { get; set; }
public string Url { get; set; }
public string Name { get; set; }
public string Block { get; set; }
public IEnumerable<ProductKey> ProductKeys { get; set; }
}
public class ProductKey
{
public int Id { get; set; }
public string Url { get; set; }
}
public class BlockResult
{
public string Name { get; set; }
public string Block { get; set; }
public int[] ProductIds { get; set; }
}