4

I have an ASP.NET page that binds a listview to an IQueryable:

<asp:ListView ID="productList" runat="server" 
    DataKeyNames="ProductID" GroupItemCount="6"
    ItemType="products.Models.Product" SelectMethod="GetProducts">
    <EmptyDataTemplate>
        <div>
            Sorry, no products available
        </div>
    </EmptyDataTemplate>
    <GroupTemplate>
        <ul id="da-thumbs" class="da-thumbs">
        <div id="itemPlaceholderContainer" runat="server" class="text-center">
            <div id="itemPlaceholder" runat="server"></div>
        </div>
        </ul>
    </GroupTemplate>
    <ItemTemplate>
        <li> 
            <img src='/products/Catalog/Images/Thumbs/<%#:Item.ImagePath%>' class="img-responsive"/>
            <div>
                <span class="hidden-xs">Short Description</span>
                <span class="spacer visible-xs"></span>
                <a href="<%#: GetRouteUrl("ProductByNameRoute", new {productName = Item.ProductName}) %>" class="btn btn-success btn-block align-mid">View Details</a>     
                <a href="<%#:Item.ShortURL %>" class="btn btn-success btn-block align-mid"><%#:Item.DownloadText%></a>
            </div>
        </li>
    </ItemTemplate>
    <LayoutTemplate>
        <div class="row">
            <asp:PlaceHolder ID="groupPlaceholderContainer" runat="server">
                <div id="groupPlaceholder" runat="server"></div>
            </asp:PlaceHolder>
        </div>                 
    </LayoutTemplate>
</asp:ListView>
<asp:DataPager ID="it" runat="server" PagedControlID="productList" PageSize="6" class="btn-group pager-buttons pagination pagination-large">
    <Fields>
        <asp:NextPreviousPagerField ShowLastPageButton="False" ShowNextPageButton="False" ButtonType="Button" ButtonCssClass="btn" RenderNonBreakingSpacesBetweenControls="false" />
        <asp:NumericPagerField ButtonType="Button" RenderNonBreakingSpacesBetweenControls="false" NumericButtonCssClass="btn" CurrentPageLabelCssClass="btn disabled" NextPreviousButtonCssClass="btn" />
        <asp:NextPreviousPagerField ShowFirstPageButton="False" ShowPreviousPageButton="False" ButtonType="Button" ButtonCssClass="btn" RenderNonBreakingSpacesBetweenControls="false" />
    </Fields>
</asp:DataPager>

GetProducts() is defined as follows:

public IQueryable<Product> GetProducts(
                        [QueryString("id")] int? categoryId,
                        [RouteData] string categoryName)
{
    var _db = new products.Models.ProductContext();
    IQueryable<Product> query = _db.Products;

    if (categoryId.HasValue && categoryId > 0)
    {
        query = query.Where(p => p.CategoryID == categoryId);
    }

    if (!String.IsNullOrEmpty(categoryName))
    {
        query = query.Where(p =>
                            String.Compare(p.Category.CategoryName,
                            categoryName) == 0);
    }

    var random = new Random();
    query = query.OrderBy(product => random.Next()).Where (p => p.Active == 1);
    return query;
}

The issue is Random() does not work here. When I run my app, I get an error

LINQ to Entities does not recognize the method 'Int32 Next()' method, and this method cannot be translated into a store expression.

Probably one solution is to change it to IEnumerable but then I need IQueryable for paging.

Update: After using Taher's solution, this is the internal query I get:

{SELECT 
    [Project1].[ProductID] AS [ProductID], 
    [Project1].[ProductName] AS [ProductName], 
    [Project1].[Description] AS [Description], 
    [Project1].[ImagePath] AS [ImagePath], 
    [Project1].[DownloadText] AS [DownloadText], 
    [Project1].[DownloadURL] AS [DownloadURL], 
    [Project1].[ShortURL] AS [ShortURL], 
    [Project1].[UnitPrice] AS [UnitPrice], 
    [Project1].[CategoryID] AS [CategoryID], 
    [Project1].[Active] AS [Active], 
    [Project1].[ShortDescription] AS [ShortDescription], 
    [Project1].[Priority] AS [Priority]
    FROM ( SELECT 
        RAND() AS [C1], 
        [Extent1].[ProductID] AS [ProductID], 
        [Extent1].[ProductName] AS [ProductName], 
        [Extent1].[Description] AS [Description], 
        [Extent1].[ImagePath] AS [ImagePath], 
        [Extent1].[DownloadText] AS [DownloadText], 
        [Extent1].[DownloadURL] AS [DownloadURL], 
        [Extent1].[ShortURL] AS [ShortURL], 
        [Extent1].[UnitPrice] AS [UnitPrice], 
        [Extent1].[CategoryID] AS [CategoryID], 
        [Extent1].[Active] AS [Active], 
        [Extent1].[ShortDescription] AS [ShortDescription], 
        [Extent1].[Priority] AS [Priority]
        FROM [dbo].[Products] AS [Extent1]
        WHERE 1 = [Extent1].[Active]
    )  AS [Project1]
    ORDER BY [Project1].[C1] ASC}

Update: Here's the modified code using Taher's and JCL's suggestions:

if (!String.IsNullOrEmpty(categoryName))
{
    Session["rand"] = null;
    query = query.Where(p =>
                        String.Compare(p.Category.CategoryName,
                        categoryName) == 0);
}

var seed = 0;

if (Session["rand"] == null)
{
    seed = (int)DateTime.Now.Ticks % 9395713;//a random number
    Session["rand"] = seed;
}

var readSeed = (int)Session["rand"];
query = query
   .OrderBy(product => SqlFunctions.Rand(product.ProductID * readSeed % 577317));
return query;
CuriousDev
  • 1,255
  • 1
  • 21
  • 44
  • You want to `order` the rows based on random property? – Taher Rahgooy Sep 04 '15 at 06:07
  • Currently the query returns the row in a particular order I think based on productId - 1, 2, 3, 4, 5 etc. I want to shuffle the results so that the results are not returned in a particular order. It could be 5, 2, 4, 1, 3 or for the next run 3, 5, 1, 4, 2 and so on. – CuriousDev Sep 04 '15 at 06:10
  • IWantToLearn your question were about error in your code, not problem with random numbers! – Arash Sep 04 '15 at 06:22
  • @Arashjo The error came at the first place because I was using Random(). I thought I made that clear in my question - "The issue is Random() does not work here." – CuriousDev Sep 04 '15 at 06:23
  • IWantToLearn instantiate random class just once and use Next method every time. – Arash Sep 04 '15 at 06:27
  • "paging" and "random" is a strange combination: you might find that page 2 contains items that were also on page 1. And when you go back to page 1, it's different! – Hans Kesting Sep 04 '15 at 08:07
  • @HansKesting exactly what I stated in my answer :-) – Jcl Sep 04 '15 at 08:13
  • How many products do you have? Is it feasible to load the full list once, shuffle it and store in Session, then use that as a basis for your paging? Do you expect a lot of simultaneous users (=sessions)? – Hans Kesting Sep 04 '15 at 08:49
  • I currently have about 25 products which will grow to 500. I currently have about 10-15 simultaneous users which may grow to 100. – CuriousDev Sep 04 '15 at 08:51
  • That's about 50.000 concurrent items in memory. If the items are not big, it's not a big deal... however I'd be wary if there's a possibility of either the products growing again, or the number of simultaneous users on the same server growing, this could escalate quickly. That said: I don't understand why you want to shuffle on a 500 product list. – Jcl Sep 04 '15 at 09:14
  • @Jcl It is like a marketplace and I want to shuffle so that I favor no particular product by displaying them on the first page. – CuriousDev Sep 04 '15 at 09:16

4 Answers4

4

There is a class of supported functions for EF called SqlFunctions. You can use the Rand function of this class:

 query = query.OrderBy(product => SqlFunctions.Rand()).Where (p => p.Active == 1);

don't forget to add

 using System.Data.Entity.Core.Objects; 

the problem with this approach is from this source:

Repetitive invocations of RAND() in a single query will produce the same value.

I don't think there is any work around for it. The solutions is to use `Newid() in raw SQL commands which I think is not useful for you, Or loading the query data and then shuffle the rows in memory.

I tried this work around and it seems to work:

var seed = (int)DateTime.Now.Ticks % 9395713;//a random number
query = query
   .OrderBy(product =>SqlFunctions.Rand(product.ProductId * seed % 577317))
   .Where (p => p.Active == 1);

this trick will force the sql server to generate a new random value for each row, as the ProductId changes on each row this changes the Rand seed which forces the sql server to generate new random value.

You can explore different values for seed variable to obtain better results.

EDIT
In order to have a seed per category, you can create a cache of seeds in Application or simply creating an static dictionary of (category, seed) pairs, something like this:

in controller if you use MVC or the page class if you use ASP.NET:

static Dictionary<string, int> categorySeeds = new Dictionary<string, int>();

in the action or the method:

int seed = 0;
if(categorySeeds.ContainsKey(categoryName))
{
   seed = categorySeeds[categoryName];
}
else
{
   seed = (int)DateTime.Now.Ticks % 9395713;//a random number
   categorySeeds.Add(categoryName, seed);
}
//rest of the code
Taher Rahgooy
  • 6,528
  • 3
  • 19
  • 30
  • I tried it but the results are not random. They are coming in the same order - var random = new Random(); query = query.OrderBy(product => SqlFunctions.Rand()).Where(p => p.Active == 1); return query; – CuriousDev Sep 04 '15 at 06:24
  • I have updated the question with the internal query I get after using your solution. Can you tell me why aren't the rows coming in a Random and what to change? – CuriousDev Sep 04 '15 at 06:30
  • 1
    Thanks @Taher the seed part does the trick but as soon as I hit page 2, the shuffle happens again. How do I make sure the shuffle happens the first time for the query, and then paginates with the rows that's now present in memory? As Hans mentioned, with the current solution, page 2 contains items that were also on page 1. And when you go back to page 1, it's different! – CuriousDev Sep 04 '15 at 08:40
  • 1
    @IWantToLearn theoretically, sending the same seed (not having one seed on every refresh, but store the seed somehow) it should always order the same – Jcl Sep 04 '15 at 08:50
  • 1
    @Jcl Any ideas on how that can be achieved using Taher's code? – CuriousDev Sep 04 '15 at 08:51
  • 2
    @IWantToLearn once set, store the `seed` variable somewhere (in your session, a cookie, a config file... I don't know, wherever), so the `SqlFunctions.Rand(product.ProductId * seed % 577317))` part always receives the same value unless you want to "shuffle again" – Jcl Sep 04 '15 at 08:53
  • That's what is a challenge to decide. I want to shuffle when the "categoryName" changes otherwise use the same seed. I am finding it hard to interpret how do I represent it in code? – CuriousDev Sep 04 '15 at 08:56
  • 1
    @IWantToLearn that *shouldn't* really be the hard part to code. A variable that persists its value till some condition happens is just extremely basic programming stuff. Where to persist that value (and if it should be per-user, per-process, per-database, or whatnot) might be conceptually difficult to choose on some occations, but the code should be basic if you know your platform – Jcl Sep 04 '15 at 09:06
  • @Jcl I have updated my question with my attempt. Can you validate to see if there is a better way? – CuriousDev Sep 04 '15 at 09:22
  • @IWantToLearn looks ok to me. You could make the code a bit more clean, but it looks ok to me, functionality-wise. Does it work for you? – Jcl Sep 04 '15 at 09:25
  • @Jcl I am precisely updating when the categoryname is *not* null or empty. Check the not operator. And yes the code works so far. – CuriousDev Sep 04 '15 at 09:26
  • @IWantToLearn yeah, I misread, sorry about that, I edited when I noticed – Jcl Sep 04 '15 at 09:27
  • No worries. Thanks for your guidance – CuriousDev Sep 04 '15 at 09:27
  • Wow @Taher that updated code looks very slick. Where exactly should I create this static Dictionary ? – CuriousDev Sep 04 '15 at 09:29
  • @IWantToLearn, anywhere in the page class, I thought you are using MVC – Taher Rahgooy Sep 04 '15 at 09:31
  • No I am using WebForms. MVC does not have a ListView :) – CuriousDev Sep 04 '15 at 09:33
  • 1
    I prefer the session-stored seed changed on category visiting, that way, the user gets a different shuffle when "coming back to a previously visited category"... but that's up to you and your design decisions, of course, @TaherRahgooy solution is also perfectly valid – Jcl Sep 04 '15 at 09:33
  • @IWantToLearn, there was an error in the code, it should be `categorySeeds.ContainsKey` not `categorySeeds.HasKey`, I corrected the anwser – Taher Rahgooy Sep 04 '15 at 09:34
  • Thanks Taher I noticed it when you were updating. Good point @Jcl – CuriousDev Sep 04 '15 at 09:36
  • I agree with @Jcl, using session seems to be a better solution. – Taher Rahgooy Sep 04 '15 at 09:37
2

I found this master piece in very similar questions How to efficiently retrieve a list of random elements from an IQueryable

users.OrderBy(_ => Guid.NewGuid()).Take(3)

This retrieves the first 3 elements from the users table, while sorting them by a value which is different each time.

Compared to AD.Net's answer.. well you'd require a list of userids generated randomly.. it doesn't suggest a way to do that

valentasm
  • 2,137
  • 23
  • 24
1

Honestly, the best possible way for this would be using a stored procedure in SQL Server to which you pass the random seed as a parameter (and change it on your code whenever you want to shuffle again).

Otherwise, unless you are passing the same seed to your random, paging will just not work (and it seems to be a requirement), since every time the query gets refreshed you'll get a different ordering.

If you can't have all your data in memory (and shuffle on in-memory objects, not on EF), the best solution would be doing it in the SQL Server directly and just have EF read from the result of that stored procedure.

Or, if modifying your entities is reasonable, then have an integer field on your entity and fill this entity with random values (should be fast if done directly via SQL, since EF as far as I know doesn't do batch updates) every time you want to shuffle, and order by that (this could be tricky in a multi-user environment, but can be done, and you shouldn't necessarily care if several different users get the same sorting, should you?).

These would be the only ways to have paging work in a truly pseudo-random/shuffled order on the server.

Again, if the table is not big (and will not be) and you can just download the whole table (without paging) and page to an in-memory collection, then that'd be easy.

Jcl
  • 27,696
  • 5
  • 61
  • 92
-2

Chage your code to this

 var random = new Random();
 var randomNumber=random.Next();
 query = query.OrderBy(product => randomNumber).Where (p => p.Active == 1);
Arash
  • 889
  • 7
  • 18
  • Although the code ran without errors, the order of the data remained the same. Currently the query returns the row in a particular order I think based on productId - 1, 2, 3, 4, 5 etc. How can I shuffle the results so that the results are not returned in a particular order. It could be 5, 2, 4, 1, 3 or for the next run 3, 5, 1, 4, 2 and so on. – CuriousDev Sep 04 '15 at 06:10
  • IWantToLearn You should keep a single Random instance and keep using Next on the same instance.Don't get instance of random class every time. – Arash Sep 04 '15 at 06:17
  • This doesn't shuffle anything. You could use `var randomnumber=4;` and have the exact same effect. The value doesn't change inside that OrderBy. – Hans Kesting Sep 04 '15 at 08:39
  • @Hans Kesting The answer is not about shuffling.look at question carefully it's about error he got, with this code , the error vanished(Although the code ran without errors..),and about shuffling number I just gave suggestion in my comment!I you really likes downvoting people ,don't you?! – Arash Sep 04 '15 at 09:07
  • 1
    The problem with this solution is the random value generated is used for all rows, thus it have no effect on ordering of rows because they all have the same value for ordering – Taher Rahgooy Sep 04 '15 at 09:19