Skip to content

Commit 3235f70

Browse files
committed
smartstore#393 Web API: Implement OData actions for simpler working with product attributes
1 parent 1279a90 commit 3235f70

10 files changed

Lines changed: 225 additions & 20 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* (Developer) Implemented _IWidgetProvider_. Allows request scoped registration of action routes to be injectable into widget zones. Perfect for custom action filters.
1313
* (Developer) Simple widgets: the model of the parent action view context now gets passed to a widget.
1414
* (Developer) New IoC method ContainerManager.InjectProperties()
15+
* #393 Web API: Implement OData actions for simpler working with product attributes
1516

1617
###Improvements###
1718
* Task Scheduler:

src/Plugins/Api.WebApi/Controllers/OData/OrdersController.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ public Order PaymentPending(int key)
102102
{
103103
var entity = GetEntityByKeyNotNull(key);
104104

105-
entity.PaymentStatus = PaymentStatus.Pending;
106-
Service.UpdateOrder(entity);
105+
this.ProcessEntity(() =>
106+
{
107+
entity.PaymentStatus = PaymentStatus.Pending;
108+
Service.UpdateOrder(entity);
109+
return null;
110+
});
107111

108112
return entity;
109113
}

src/Plugins/Api.WebApi/Controllers/OData/ProductsController.cs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
using SmartStore.Web.Framework.WebApi;
1313
using SmartStore.Web.Framework.WebApi.OData;
1414
using SmartStore.Web.Framework.WebApi.Security;
15+
using System.Web.Http.OData;
16+
using SmartStore.Plugin.Api.WebApi.Services;
17+
using System.Web.Http.OData.Query;
1518

1619
namespace SmartStore.Plugin.Api.WebApi.Controllers.OData
1720
{
@@ -21,15 +24,18 @@ public class ProductsController : WebApiEntityController<Product, IProductServic
2124
private readonly Lazy<IWorkContext> _workContext;
2225
private readonly Lazy<IPriceCalculationService> _priceCalculationService;
2326
private readonly Lazy<IUrlRecordService> _urlRecordService;
27+
private readonly Lazy<IProductAttributeService> _productAttributeService;
2428

2529
public ProductsController(
2630
Lazy<IWorkContext> workContext,
2731
Lazy<IPriceCalculationService> priceCalculationService,
28-
Lazy<IUrlRecordService> urlRecordService)
32+
Lazy<IUrlRecordService> urlRecordService,
33+
Lazy<IProductAttributeService> productAttributeService)
2934
{
3035
_workContext = workContext;
3136
_priceCalculationService = priceCalculationService;
3237
_urlRecordService = urlRecordService;
38+
_productAttributeService = productAttributeService;
3339
}
3440

3541
protected override IQueryable<Product> GetEntitySet()
@@ -218,6 +224,125 @@ public IQueryable<ProductBundleItem> GetProductBundleItems(int key)
218224
return CalculatePrice(key, true);
219225
}
220226

227+
[HttpPost, WebApiQueryable(PagingOptional = true)]
228+
public IQueryable<ProductVariantAttributeCombination> CreateAttributeCombinations(int key)
229+
{
230+
var entity = GetEntityByKeyNotNull(key);
231+
232+
this.ProcessEntity(() =>
233+
{
234+
_productAttributeService.Value.CreateAllProductVariantAttributeCombinations(entity);
235+
return null;
236+
});
237+
238+
return entity.ProductVariantAttributeCombinations.AsQueryable();
239+
}
240+
241+
[HttpPost, WebApiQueryable(PagingOptional = true)]
242+
public IQueryable<ProductVariantAttribute> ManageAttributes(int key, ODataActionParameters parameters)
243+
{
244+
var entity = GetExpandedEntity<ICollection<ProductVariantAttribute>>(key, x => x.ProductVariantAttributes);
245+
var result = new List<ProductVariantAttributeValue>();
246+
247+
this.ProcessEntity(() =>
248+
{
249+
bool synchronize = parameters.GetValue<string, bool>("Synchronize");
250+
var data = (parameters["Attributes"] as IEnumerable<ManageAttributeType>).Where(x => x.Name.HasValue()).ToList();
251+
252+
var allAttributes = _productAttributeService.Value.GetAllProductAttributes();
253+
254+
foreach (var srcAttr in data)
255+
{
256+
var productAttribute = allAttributes.FirstOrDefault(x => x.Name.IsCaseInsensitiveEqual(srcAttr.Name));
257+
258+
if (productAttribute == null)
259+
{
260+
productAttribute = new ProductAttribute() { Name = srcAttr.Name };
261+
_productAttributeService.Value.InsertProductAttribute(productAttribute);
262+
}
263+
264+
var attribute = entity.ProductVariantAttributes.FirstOrDefault(x => x.ProductAttribute.Name.IsCaseInsensitiveEqual(srcAttr.Name));
265+
266+
if (attribute == null)
267+
{
268+
attribute = new ProductVariantAttribute()
269+
{
270+
ProductId = entity.Id,
271+
ProductAttributeId = productAttribute.Id,
272+
AttributeControlTypeId = srcAttr.ControlTypeId,
273+
DisplayOrder = entity.ProductVariantAttributes.OrderByDescending(x => x.DisplayOrder).Select(x => x.DisplayOrder).FirstOrDefault() + 1,
274+
IsRequired = srcAttr.IsRequired
275+
};
276+
277+
entity.ProductVariantAttributes.Add(attribute);
278+
Service.UpdateProduct(entity);
279+
}
280+
else if (synchronize)
281+
{
282+
if (srcAttr.Values.Count <= 0)
283+
{
284+
_productAttributeService.Value.DeleteProductVariantAttribute(attribute);
285+
}
286+
else
287+
{
288+
attribute.AttributeControlTypeId = srcAttr.ControlTypeId;
289+
attribute.IsRequired = srcAttr.IsRequired;
290+
291+
Service.UpdateProduct(entity);
292+
}
293+
}
294+
295+
int maxDisplayOrder = attribute.ProductVariantAttributeValues.OrderByDescending(x => x.DisplayOrder).Select(x => x.DisplayOrder).FirstOrDefault();
296+
297+
foreach (var srcVal in srcAttr.Values.Where(x => x.Name.HasValue()))
298+
{
299+
var value = attribute.ProductVariantAttributeValues.FirstOrDefault(x => x.Name.IsCaseInsensitiveEqual(srcVal.Name));
300+
301+
if (value == null)
302+
{
303+
value = new ProductVariantAttributeValue()
304+
{
305+
ProductVariantAttributeId = attribute.Id,
306+
Name = srcVal.Name,
307+
Alias = srcVal.Alias,
308+
ColorSquaresRgb = srcVal.ColorSquaresRgb,
309+
PriceAdjustment = srcVal.PriceAdjustment,
310+
WeightAdjustment = srcVal.WeightAdjustment,
311+
IsPreSelected = srcVal.IsPreSelected,
312+
DisplayOrder = ++maxDisplayOrder
313+
};
314+
315+
attribute.ProductVariantAttributeValues.Add(value);
316+
Service.UpdateProduct(entity);
317+
}
318+
else if (synchronize)
319+
{
320+
value.Alias = srcVal.Alias;
321+
value.ColorSquaresRgb = srcVal.ColorSquaresRgb;
322+
value.PriceAdjustment = srcVal.PriceAdjustment;
323+
value.WeightAdjustment = srcVal.WeightAdjustment;
324+
value.IsPreSelected = srcVal.IsPreSelected;
325+
326+
Service.UpdateProduct(entity);
327+
}
328+
}
329+
330+
if (synchronize)
331+
{
332+
foreach (var dstVal in attribute.ProductVariantAttributeValues.ToList())
333+
{
334+
if (!srcAttr.Values.Exists(x => x.Name.IsCaseInsensitiveEqual(dstVal.Name)))
335+
_productAttributeService.Value.DeleteProductVariantAttributeValue(dstVal);
336+
}
337+
}
338+
}
339+
return null;
340+
});
341+
342+
return entity.ProductVariantAttributes.AsQueryable();
343+
}
344+
345+
221346
//[HttpGet, WebApiQueryable]
222347
//public IQueryable<RelatedProduct> GetRelatedProducts(int key)
223348
//{

src/Plugins/Api.WebApi/Description.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Group: Api
22
FriendlyName: SmartStore.NET Web Api
33
SystemName: Api.WebApi
4-
Version: 1.21
4+
Version: 1.22
55
MinAppVersion: 2.0.0
66
Author: SmartStore AG
77
DisplayOrder: 1

src/Plugins/Api.WebApi/Docs/Developer-Guide.md

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,46 @@ Dynamic JSON parsing might look like this:
377377
Debug.WriteLine(str);
378378
}
379379

380-
380+
381+
# Working with the product entity
382+
383+
#### Getting a product with name *SmartStore eBay SmartSeller*
384+
385+
GET http://localhost:1260/odata/v1/Products?$top=1&$filter=Name eq 'SmartStore eBay SmartSeller'
386+
387+
#### Get child products of group product with ID 210
388+
389+
GET http://localhost:1260/odata/v1/Products?$top=120&$filter=ParentGroupedProductId eq 210
390+
391+
#### Get final price of product with ID 211
392+
393+
POST http://localhost:1260/odata/v1/Products(211)/FinalPrice
394+
395+
Note the post method. There is a second action `LowestPrice` which serves the lowest possible price for a product.
396+
397+
#### Managing product attributes
398+
399+
You can use the following endpoints: `ProductAttributes` (types of attributes), `ProductVariantAttributes` (attribute types mapped to a product), `ProductVariantAttributeValues` (attribute values assigned to product) and optionally `ProductVariantAttributeCombinations` (additional information for particular attribute combinations). Because managing attributes that way can lead to some extra work, there is an action method `ManageAttributes` that wraps up the most important steps.
400+
401+
POST http://localhost:1260/odata/v1/Products(211)/ManageAttributes
402+
{"Synchronize":true,"Attributes":[
403+
{"Name":"Color","IsRequired":false,"Values":[
404+
{"Name":"Red"},{"Name":"Green","IsPreSelected":true},{"Name":"Blue"}
405+
]},
406+
{"Name":"Size","Values":[
407+
{"Name":"Large"},{"Name":"X-Large","IsPreSelected":true}
408+
]}
409+
]}
410+
411+
That request configurates a product with the ID 211 with two attributes `Color` and `Size` and its values `Red, Green, Blue` and `Large, X-Large`. The action parameter `Attributes` is of type `ManageAttributeType`. See the OData metadata document for a complete list of available properties. If `Synchronize` is set to `false` only missing attributes and attribute values are inserted. If set to `true` existing records are also updated and values that are not included in the request body are removed from the database. If you pass an empty value array the attribute and all its values are removed from the product.
412+
413+
The following method creates all possible attribute combinations for a product with the ID 211
414+
415+
POST http://localhost:1260/odata/v1/Products(211)/CreateAttributeCombinations
416+
417+
As a first step this action always deletes all existing attribute combinations (for a product).
418+
419+
381420
# More examples
382421

383422
#### Get installed payment methods
@@ -416,20 +455,6 @@ This request is called OData navigation. Use `BillingAddress` if you want the bi
416455
The example uses SmNetFulfillCountry and SmNetFulfillStateProvince options to update the country (USA) and province (New York). That avoids extra querying of country and province records and passing its IDs in the request body.
417456
Note that you cannot use a path `/Orders(145)/ShippingAddress` to update an address because `ShippingAddress` is a navigation property and updates are not supported here.
418457

419-
#### Get product with name *SmartStore eBay SmartSeller*
420-
421-
GET http://localhost:1260/odata/v1/Products?$top=1&$filter=Name eq 'SmartStore eBay SmartSeller'
422-
423-
#### Get child products of group product with ID 210
424-
425-
GET http://localhost:1260/odata/v1/Products?$top=120&$filter=ParentGroupedProductId eq 210
426-
427-
#### Get final price of product with ID 211
428-
429-
POST http://localhost:1260/odata/v1/Products(211)/FinalPrice
430-
431-
Note the post method. `FinalPrice` is an OData action because further data processing (price calculation) is required. There is a second action `LowestPrice` which serves the lowest possible price for a product.
432-
433458
#### Get email address of customer with ID 1
434459

435460
GET http://localhost:1260/odata/v1/Customers(1)/Email
@@ -442,6 +467,7 @@ Note the select option which tells OData just to return the Id property.
442467

443468

444469

470+
445471
<br />
446472
[1]: http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-a-net-client
447473
[2]: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using SmartStore.Core.Domain.Catalog;
4+
5+
namespace SmartStore.Plugin.Api.WebApi.Services
6+
{
7+
[Serializable]
8+
internal class ManageAttributeType
9+
{
10+
public ManageAttributeType()
11+
{
12+
Values = new List<ManageAttributeValue>();
13+
ControlTypeId = (int)AttributeControlType.DropdownList;
14+
IsRequired = true;
15+
}
16+
17+
public string Name { get; set; }
18+
public int ControlTypeId { get; set; }
19+
public bool IsRequired { get; set; }
20+
21+
public IList<ManageAttributeValue> Values { get; set; }
22+
23+
public class ManageAttributeValue
24+
{
25+
public string Name { get; set; }
26+
public string Alias { get; set; }
27+
public string ColorSquaresRgb { get; set; }
28+
public decimal PriceAdjustment { get; set; }
29+
public decimal WeightAdjustment { get; set; }
30+
public bool IsPreSelected { get; set; }
31+
}
32+
}
33+
}

src/Plugins/Api.WebApi/SmartStore.Plugin.Api.WebApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
<Compile Include="RouteProvider.cs" />
221221
<Compile Include="Security\WebApiPermissionProvider.cs" />
222222
<Compile Include="Services\IWebApiPluginService.cs" />
223+
<Compile Include="Services\WebApiCore.cs" />
223224
<Compile Include="Services\WebApiPluginService.cs" />
224225
<Compile Include="WebApiConfigurationProvider.cs" />
225226
<Compile Include="WebApiPlugin.cs" />

src/Plugins/Api.WebApi/WebApiConfigurationProvider.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using SmartStore.Core.Domain.Stores;
1313
using SmartStore.Web.Framework.WebApi.Configuration;
1414
using System.Web.Http.OData.Builder;
15+
using SmartStore.Plugin.Api.WebApi.Services;
1516

1617
namespace SmartStore.Plugin.Api.WebApi
1718
{
@@ -40,6 +41,15 @@ private void AddActionsToProduct(EntityTypeConfiguration<Product> config)
4041

4142
config.Action("LowestPrice")
4243
.Returns<decimal>();
44+
45+
config.Action("CreateAttributeCombinations")
46+
.ReturnsCollectionFromEntitySet<ProductVariantAttributeCombination>(WebApiOdataEntitySet.ProductVariantAttributeCombinations);
47+
48+
var manageAttributes = config.Action("ManageAttributes")
49+
.ReturnsCollectionFromEntitySet<ProductVariantAttribute>(WebApiOdataEntitySet.ProductVariantAttributes);
50+
51+
manageAttributes.Parameter<bool>("Synchronize");
52+
manageAttributes.CollectionParameter<ManageAttributeType>("Attributes");
4353
}
4454

4555
public void Configure(WebApiConfigurationBroadcaster configData)

src/Plugins/Api.WebApi/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#Release Notes#
22

3+
##Web Api 1.22##
4+
5+
###New Features###
6+
* #393 Implement OData actions for simpler working with product attributes -> added ProductsController.ManageAttributes and ProductsController.CreateAttributeCombinations.
7+
38
##Web Api 1.21##
49

510
###Improvements###

src/Presentation/SmartStore.Web.Framework/WebApi/WebApiEntityController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected internal HttpResponseException ExceptionNotExpanded<TProperty>(Express
4040

4141
public override HttpResponseMessage HandleUnmappedRequest(ODataPath odataPath)
4242
{
43-
if (Request.Method == HttpMethod.Get)
43+
if (Request.Method == HttpMethod.Get || Request.Method == HttpMethod.Post)
4444
{
4545
if (odataPath.PathTemplate.IsCaseInsensitiveEqual("~/entityset/key/property") ||
4646
odataPath.PathTemplate.IsCaseInsensitiveEqual("~/entityset/key/cast/property") ||

0 commit comments

Comments
 (0)