/GoogleProductFeed

Primary LanguageC#Apache License 2.0Apache-2.0

Geta Google Product Feed

  • Master
    Platform Platform Platform

Credits: How to make a Google Shopping Feed with C# and serve it through the Web API.

This will create a Google Product Feed based on the Atom specification. For information on what is required and what the different attributes/properties mean, please see the Product data specification.

Installation

Install-Package Geta.GoogleProductFeed

Note that you need to make sure your projects calls config.MapHttpAttributeRoutes(); in order for the feed routing to work.

Default URL is: /googleproductfeed

FeedBuilder

You need to implement the abstract class FeedBuilder and the method Build. This will provide the feed data. Build method returns List of feeds, this is required so that FeedBuilder can produce feeds for both multisite and singlesite projects. Example bellow can be extended to support multisite projects.

Default FeedBuilder

You can iherit from default base feed builder class (DefaultFeedBuilderBase) which will help you get started. It contains CatalogEntry enumeration code and sample error handling. You will need to implement following methods:

protected abstract Feed GenerateFeedEntity();

protected abstract Entry GenerateEntry(CatalogContentBase catalogContent);

For example:

public class EpiFeedBuilder : DefaultFeedBuilderBase
{
    private readonly IPricingService _pricingService;
    private readonly Uri _siteUri;

    public EpiFeedBuilder(
        IContentLoader contentLoader,
        ReferenceConverter referenceConverter,
        IPricingService pricingService,
        ISiteDefinitionRepository siteDefinitionRepository,
        IContentLanguageAccessor languageAccessor) : base(contentLoader, referenceConverter, languageAccessor)
    {
        _pricingService = pricingService;
        _siteUri = siteDefinitionRepository.List().FirstOrDefault()?.Hosts.GetPrimaryHostDefinition().Url;
    }

    protected override Feed GenerateFeedEntity()
    {
        return new Feed
        {
            Updated = DateTime.UtcNow,
            Title = "My products",
            Link = _siteUri.ToString()
        };
    }

    protected override Entry GenerateEntry(CatalogContentBase catalogContent)
    {
        return ...;
    }

    private HostDefinition GetPrimaryHostDefinition(IList<HostDefinition> hosts)
    {
        if (hosts == null)
        {
            throw new ArgumentNullException(nameof(hosts));
        }

        return hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Primary && !h.IsWildcardHost())
               ?? hosts.FirstOrDefault(h => !h.IsWildcardHost());
    }
}

Implement Your Own Builder

If you need more flexible solution to build Google Product Feed - you can implement whole builder yourself. Below is given sample feed builder (based on Quicksilver demo project). Please use it as starting point and adjust things that you need to customize. Also keep in mind that for example error handling is not implemented in this sample (which means if variation generation fails - job will be aborted and feed will not be generated at all).

public class EpiFeedBuilder : FeedBuilder
{
    private readonly IContentLoader _contentLoader;
    private readonly ReferenceConverter _referenceConverter;
    private readonly IPricingService _pricingService;
    private readonly ILogger _logger;
    private readonly IContentLanguageAccessor _languageAccessor;
    private readonly Uri _siteUri;

    public EpiFeedBuilder(
        IContentLoader contentLoader,
        ReferenceConverter referenceConverter,
        IPricingService pricingService,
        ISiteDefinitionRepository siteDefinitionRepository,
        IContentLanguageAccessor languageAccessor)
    {
        _contentLoader = contentLoader;
        _referenceConverter = referenceConverter;
        _pricingService = pricingService;
        _logger = LogManager.GetLogger(typeof(EpiFeedBuilder));
        _languageAccessor = languageAccessor;
        _siteUri = GetPrimaryHostDefinition(siteDefinitionRepository.List().FirstOrDefault()?.Hosts)?.Url;
    }

    public override List<Feed> Build()
    {
        List<Feed> generatedFeeds = new List<Feed>();
        Feed feed = new Feed
        {
            Updated = DateTime.UtcNow,
            Title = "My products",
            Link = _siteUri.ToString()
        };

        IEnumerable<ContentReference> catalogReferences = _contentLoader.GetDescendents(_referenceConverter.GetRootLink());
        IEnumerable<CatalogContentBase> items = _contentLoader.GetItems(catalogReferences, CreateDefaultLoadOption()).OfType<CatalogContentBase>();

        List<Entry> entries = new List<Entry>();
        foreach (CatalogContentBase catalogContent in items)
        {
            FashionVariant variationContent = catalogContent as FashionVariant;

            try
            {
                if (variationContent == null)
                    continue;

                FashionProduct product = _contentLoader.Get<CatalogContentBase>(variationContent.GetParentProducts().FirstOrDefault()) as FashionProduct;
                string variantCode = variationContent.Code;
                IPriceValue defaultPrice = _pricingService.GetPrice(variantCode);

                Entry entry = new Entry
                {
                    Id = variationContent.Code,
                    Title = variationContent.DisplayName,
                    Description = product?.Description.ToHtmlString(),
                    Link = variationContent.GetUrl(),
                    Condition = "new",
                    Availability = "in stock",
                    Brand = product?.Brand,
                    MPN = "",
                    GTIN = "...",
                    GoogleProductCategory = "",
                    Shipping = new List<Shipping>
                    {
                        new Shipping
                        {
                            Price = "Free",
                            Country = "US",
                            Service = "Standard"
                        }
                    }
                };

                string image = variationContent.GetDefaultAsset<IContentImage>();

                if (!string.IsNullOrEmpty(image))
                {
                    entry.ImageLink = Uri.TryCreate(_siteUri, image, out Uri imageUri) ? imageUri.ToString() : image;
                }

                if (defaultPrice != null)
                {
                    IPriceValue discountPrice = _pricingService.GetDiscountPrice(variantCode);

                    entry.Price = defaultPrice.UnitPrice.ToString();
                    entry.SalePrice = discountPrice.ToString();
                    entry.SalePriceEffectiveDate = $"{DateTime.UtcNow:yyyy-MM-ddThh:mm:ss}/{DateTime.UtcNow.AddDays(7):yyyy-MM-ddThh:mm:ss}";
                }

                entries.Add(entry);
            }
            catch (Exception ex)
            {
                _logger.Error($"Failed to generate feed item for catalog entry ({catalogContent.ContentGuid})", ex);
            }
        }

        feed.Entries = entries;
        generatedFeeds.Add(feed);

        return generatedFeeds;
    }

    private HostDefinition GetPrimaryHostDefinition(IList<HostDefinition> hosts)
    {
        if (hosts == null)
        {
            throw new ArgumentNullException(nameof(hosts));
        }

        return hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Primary && !h.IsWildcardHost())
                ?? hosts.FirstOrDefault(h => !h.IsWildcardHost());
    }

    private LoaderOptions CreateDefaultLoadOption()
    {
        LoaderOptions loaderOptions = new LoaderOptions
        {
            LanguageLoaderOption.FallbackWithMaster(_languageAccessor.Language)
        };

        return loaderOptions;
    }
}

Register Builder in IoC

Then you need to use this as the default implementation for FeedBuilder. Using StructureMap it will look something like this in your registry class:

For<FeedBuilder>().Use<EpiFeedBuilder>();

Make sure dependency injection is setup for Web API. The quickest way to do this is install the package: Foundation.WebApi.

Feed Generation

Populating the feed is handled through a scheduled job and the result is serialized and stored in the database. See job Google ProductFeed - Create feed in admin mode.

Troubleshooting

If your request to /googleproductfeed returns 404 with message No feed generated, make sure you run the job to populate the feed.

Local development setup

See description in shared repository regarding how to setup local development environment.

Docker hostnames

Instead of using the static IP addresses the following hostnames can be used out-of-the-box.

http://googleproductfeed.getalocaltest.me http://manager-googleproductfeed.getalocaltest.me

Package maintainer

https://github.com/valdisiljuconoks

Changelog

Changelog