How to add an additional output column for ProductLoadAllPaged stored procedure NC 4.2

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.
3 years ago
I've had the need to add an additional product column (computed via a UDF) and am not sure how to retrieve it with other product columns.
The stored proc portion of interest is here:
--return products
  SELECT TOP (@RowsToReturn)
  p.[Id]
      ,p.[ProductTypeId]
      ,p.[ParentGroupedProductId]
      ,p.[VisibleIndividually]
      ,p.[Name]
      ,p.[ShortDescription]
      ,p.[FullDescription]
      ,p.[AdminComment]
      ,p.[ProductTemplateId]
      ,p.[VendorId]
      ,p.[ShowOnHomepage]
      ,p.[MetaKeywords]
      ,p.[MetaDescription]
      ,p.[MetaTitle]
      ,p.[AllowCustomerReviews]
      ,p.[ApprovedRatingSum]
      ,p.[NotApprovedRatingSum]
      ,p.[ApprovedTotalReviews]
      ,p.[NotApprovedTotalReviews]
      ,p.[SubjectToAcl]
      ,p.[LimitedToStores]
      ,p.[Sku]
      ,p.[ManufacturerPartNumber]
      ,p.[Gtin]
      ,p.[IsGiftCard]
      ,p.[GiftCardTypeId]
      ,p.[OverriddenGiftCardAmount]
      ,p.[RequireOtherProducts]
      ,p.[RequiredProductIds]
      ,p.[AutomaticallyAddRequiredProducts]
      ,p.[IsDownload]
      ,p.[DownloadId]
      ,p.[UnlimitedDownloads]
      ,p.[MaxNumberOfDownloads]
      ,p.[DownloadExpirationDays]
      ,p.[DownloadActivationTypeId]
      ,p.[HasSampleDownload]
      ,p.[SampleDownloadId]
      ,p.[HasUserAgreement]
      ,p.[UserAgreementText]
      ,p.[IsRecurring]
      ,p.[RecurringCycleLength]
      ,p.[RecurringCyclePeriodId]
      ,p.[RecurringTotalCycles]
      ,p.[IsRental]
      ,p.[RentalPriceLength]
      ,p.[RentalPricePeriodId]
      ,p.[IsShipEnabled]
      ,dbo.GetIsFreeShippingFromFirstVariant(p.[Id],p.[IsFreeShipping],p.[ProductTypeId]) As [IsFreeShipping]
      ,p.[ShipSeparately]
      ,p.[AdditionalShippingCharge]
      ,p.[DeliveryDateId]
      ,p.[IsTaxExempt]
      ,p.[TaxCategoryId]
      ,p.[IsTelecommunicationsOrBroadcastingOrElectronicServices]
      ,p.[ManageInventoryMethodId]
      ,p.[ProductAvailabilityRangeId]
      ,p.[UseMultipleWarehouses]
      ,p.[WarehouseId]
      ,p.[StockQuantity]
      ,p.[DisplayStockAvailability]
      ,p.[DisplayStockQuantity]
      ,p.[MinStockQuantity]
      ,p.[LowStockActivityId]
      ,p.[NotifyAdminForQuantityBelow]
      ,p.[BackorderModeId]
      ,p.[AllowBackInStockSubscriptions]
      ,p.[OrderMinimumQuantity]
      ,p.[OrderMaximumQuantity]
      ,p.[AllowedQuantities]
      ,p.[AllowAddingOnlyExistingAttributeCombinations]
      ,p.[NotReturnable]
      ,p.[DisableBuyButton]
      ,p.[DisableWishlistButton]
      ,p.[AvailableForPreOrder]
      ,p.[PreOrderAvailabilityStartDateTimeUtc]
      ,p.[CallForPrice]
      ,p.[Price]
      ,p.[OldPrice]
      ,p.[ProductCost]
      ,p.[CustomerEntersPrice]
      ,p.[MinimumCustomerEnteredPrice]
      ,p.[MaximumCustomerEnteredPrice]
      ,p.[BasepriceEnabled]
      ,p.[BasepriceAmount]
      ,p.[BasepriceUnitId]
      ,p.[BasepriceBaseAmount]
      ,p.[BasepriceBaseUnitId]
      ,p.[MarkAsNew]
      ,p.[MarkAsNewStartDateTimeUtc]
      ,p.[MarkAsNewEndDateTimeUtc]
      ,p.[HasTierPrices]
      ,p.[HasDiscountsApplied]
      ,p.[Weight]
      ,p.[Length]
      ,p.[Width]
      ,p.[Height]
      ,p.[AvailableStartDateTimeUtc]
      ,p.[AvailableEndDateTimeUtc]
      ,p.[DisplayOrder]
      ,p.[Published]
      ,p.[Deleted]
      ,p.[CreatedOnUtc]
      ,p.[UpdatedOnUtc]
,[dbo].[GetTotalSqFtAvailable](p.Id,p.StockQuantity) AS SqFtInStock  
  FROM
    #PageIndex [pi]
    INNER JOIN Product p with (NOLOCK) on p.Id = [pi].[ProductId]
  WHERE
    [pi].IndexId > @PageLowerBound AND
    [pi].IndexId < @PageUpperBound
  ORDER BY
    [pi].IndexId


Note: I changed the select * to select p.columnname for each so I could add another column outputted called SqFtInStock (a UDF that computes a value for display later)

If I edit the product.cs file and put in
        ///<summary>
        /// SqftInStock
        /// </summary>
        public decimal SqFtInStock { get; set; }


When running, I get the following error:
SqlException: Invalid column name 'SqFtInStock'.
System.Data.SqlClient.SqlConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)


I'm not sure why this is happening nor where else I need to make changes besides the Product entity and can't use EF because this column does not exist in the Product table.
Anyone have any idea all the different classes that need to be manually changed to add this extra column?
3 years ago
OK - I'm stuck at this point:
//invoke stored procedure
            var products = _dbContext.EntityFromSql<Product>("ProductLoadAllPaged",   ...  ....

The EntityFromSql part is probably the problem.  Where is Product defined?  I realize there is some EF magic going on, but am stumped as to where to look. Is there an external file somewhere that defines the Product object (so I can add another column to it)?  
Any experts out there?  

All I wish to do is show SqFtInStock on the catalog pages - I can do this by calling a function straight from razor on the page and bypass the Model, but for a page of 30 products, that would be 30 separate calls to the database - so I don't wish to do it this way.  Any change I make to the model results in the error above (so, clearly, I'm not making changes in the right places)

Anyone?
3 years ago
src\Libraries\Nop.Core\Domain\Catalog\Product.cs defines Product

As it is Decimal add property in Map
src\Libraries\Nop.Data\Mapping\Catalog\ProductMap.cs
3 years ago
Thanks for the tip, however, I'm getting the same error.
It's crashing at
        public virtual IList<Product> GetAllProductsDisplayedOnHomepage()
        {
            var query = from p in _productRepository.Table
                        orderby p.DisplayOrder, p.Id
                        where p.Published &&
                        !p.Deleted &&
                        p.ShowOnHomepage
                        select p;
            var products = query.ToList();
            return products;
        }

the productRepository is the problem, and I can follow it to where it gets loaded somewhere by EF, but I can't find where it actually gets loaded or where the (product) Entity is defined (simply adding a definition to the product class doesn't work).
Somewhere the productRepository (table) gets populated by EF, but I can't find it anywhere (there are dozens of references to it, but only as joins to _productRepository.Table  (it's loaded somewhere!)

I just wanted to add that starting the website in VS2019, it never actually hits the function
var products = _dbContext.EntityFromSql<Product>("ProductLoadAllPaged", ... 

I believe it is attempting to load homepage products (there are none, but it's trying, regardless) via another method altogether - it doesn't use the stored proc call.

Here's the complete error message:
SqlException: Invalid column name 'SqFtInStock'.
System.Data.SqlClient.SqlConnection.OnError(SqlException exception, bool breakConnection, Action<Action> wrapCloseInAction)
System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool callerHasConnectionLock, bool asyncClose)
System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, out bool dataReady)
System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
System.Data.SqlClient.SqlDataReader.get_MetaData()
System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, string resetOptionsString)
System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, bool returnStream, bool async, int timeout, out Task task, bool asyncWrite, SqlDataReader ds)
System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.Execute(IRelationalConnection connection, DbCommandMethod executeMethod, IReadOnlyDictionary<string, object> parameterValues)
Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.ExecuteReader(IRelationalConnection connection, IReadOnlyDictionary<string, object> parameterValues)
Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable<T>+Enumerator.BufferlessMoveNext(DbContext _, bool buffer)
Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute<TState, TResult>(TState state, Func<DbContext, TState, TResult> operation, Func<DbContext, TState, ExecutionResult<TResult>> verifySucceeded)
Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable<T>+Enumerator.MoveNext()
Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider._TrackEntities<TOut, TIn>(IEnumerable<TOut> results, QueryContext queryContext, IList<EntityTrackingInfo> entityTrackingInfos, IList<Func<TIn, object>> entityAccessors)+MoveNext()
Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ExceptionInterceptor<T>+EnumeratorExceptionInterceptor.MoveNext()
System.Collections.Generic.List<T>.AddEnumerable(IEnumerable<T> enumerable)
System.Linq.Enumerable.ToList<TSource>(IEnumerable<TSource> source)
Nop.Services.Catalog.ProductService.GetAllProductsDisplayedOnHomepage() in ProductService.cs
+
            var products = query.ToList();
Nop.Web.Components.HomepageProductsViewComponent.Invoke(Nullable<int> productThumbPictureSize) in HomepageProducts.cs
+
            var products = _productService.GetAllProductsDisplayedOnHomepage();
lambda_method(Closure , object , object[] )
Microsoft.AspNetCore.Mvc.ViewComponents.DefaultViewComponentInvoker.InvokeSyncCore(ObjectMethodExecutor executor, ViewComponentContext context)
Microsoft.AspNetCore.Mvc.ViewComponents.DefaultViewComponentInvoker.InvokeAsync(ViewComponentContext context)
StackExchange.Profiling.Internal.ProfilingViewComponentInvoker.InvokeAsync(ViewComponentContext context) in ProfilingViewComponentInvoker.cs
Microsoft.AspNetCore.Mvc.ViewComponents.DefaultViewComponentHelper.InvokeCoreAsync(ViewComponentDescriptor descriptor, object arguments)
AspNetCore.Themes_iCarpetiles_Views_Home_Index.ExecuteAsync() in Index.cshtml
+
        @await Component.InvokeAsync("HomepageProducts")
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, bool invokeViewStarts)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync<TFilter, TFilterAsync>()
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
WebMarkupMin.AspNetCore2.WebMarkupMinMiddleware.Invoke(HttpContext context)
StackExchange.Profiling.MiniProfilerMiddleware.Invoke(HttpContext context) in MiniProfilerMiddleware.cs
Nop.Services.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) in AuthenticationMiddleware.cs
+
            await _next(context);
Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
Nop.Services.Installation.InstallUrlMiddleware.Invoke(HttpContext context, IWebHelper webHelper) in InstallUrlMiddleware.cs
+
            await _next(context);
Nop.Services.Common.KeepAliveMiddleware.Invoke(HttpContext context, IWebHelper webHelper) in KeepAliveMiddleware.cs
+
            await _next(context);
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
Nop.Web.Framework.Infrastructure.Extensions.ApplicationBuilderExtensions+<>c.<UseNopExceptionHandler>b__1_1(HttpContext context) in ApplicationBuilderExtensions.cs
+
                        ExceptionDispatchInfo.Throw(exception);
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
3 years ago
I've narrowed it down to EntityFramework as the culprit.  
It affects any function that uses _productRepository (like for associated products, best sellers, home page products, etc).
This still leaves me stuck - I can't find where the productRepository is loaded and why the product object is completely ignored!   Is part of the source code missing? Is the old definition cached somewhere?
3 years ago
If you're going add your new property to the Product class, then also add a new column to the Product table in the database.  In the stored procedure, since you are SELECTing each column name, you can 'override' the column with your UDF.

(P.S.  Calling UDFs are not so efficient.  If you could instead JOIN to your stock table, that would be better.)
3 years ago
Note: that the routine with the error is reading the Product straight from the table not through the Stored Procedure

_productRepository is just a repository of Type Product

        private readonly IRepository<Product> _productRepository;

You would need to add that field SqFtInStock to the Product Table for it to work

I don’t know where you are planning to use the field
If it is just a function of the Stored Procedure then maybe use an abstract class to read the Procedure call data into  

public class ProductSqFtInStock
{
    Product product  
    Decimal SqFtInStock
}

var products = _dbContext.EntityFromSql<ProductSqFtInStock>

Otherwise if it is everywhere the other product fields are used then you may need to add that SqFtInStock data to the product table itself for the case where the Product is read straight from the Table rather than through the Stored Procedure
3 years ago
Ah, I see. That makes sense now.  (i was having my suspicions)
Unfortunately, that's a kludgy solution (adding unnecessary columns to a table just to make EF play nice).   I'll have to rewrite a ton of code just to do this OR add superfluous columns to the table.
That was just one column - I've several more output columns that are also derivations from multiple tables that have no associated table column.
As far as the UDF goes - I only use them when a join is not feasible, would result in duplicates or to replace a cursor.
I'm going to try & create another class that inherits from the product class for use specifically for the category/manufacturer product listings and leave the rest of the EF code alone.
Thanks All for your help!
3 years ago
RE: "... I can do this by calling a function straight from razor on the page and bypass the Model, but for a page of 30 products, that would be 30 separate calls to the database - so I don't wish to do it this way.  ".

Maybe a bit of a hack, but you could create your own stored procedure to return all your columns.  One call with a CSV list of product ids.  If you have SQL 2016 or greater use you can use STRING_SPLIT - e.g.
SELECT * FROM Product WHERE Id in (SELECT value FROM STRING_SPLIT('1,2,3,4', ','))
3 years ago
Yeah, I just discovered that EntityFromSql also requires the stored proc output columns be in the stupid table.  I really hate EF - what a crippling handicap to have.  Ugh!   I think I'm going to completely bypass it altogether and write a category system from scratch.  I wish I knew this horror beforehand as I would not have decided on NC for a new cart since it's so difficult to expand.  
I'll just make a damn ADO call and get this project finished.  Need 6 more values that are NOT in the product table and computed on the fly - I cannot just put stuff into the table since the computations require multi-table access (hence UDFs) and, since it's not possible to do via EF (without rewriting the entire cart).
I just spent a day coding another class, ProductExt that inherits from product and made changes to about 50 places and overloaded functions, got it to compile, and BANG! found the FromSql function has this restriction.  A whole day in vain!
This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.