Skip to content

Instantly share code, notes, and snippets.

@RWaltersMA
Last active November 9, 2020 14:42
Show Gist options
  • Save RWaltersMA/70f7cc615d9472d3af6d49d11ec69dd8 to your computer and use it in GitHub Desktop.
Save RWaltersMA/70f7cc615d9472d3af6d49d11ec69dd8 to your computer and use it in GitHub Desktop.
Azure Function - HTTP Trigger that leverages MongoDB Atlas database
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Bson.Serialization.Attributes;
using System.Collections.Generic;
namespace MongoOnlineGrocery
{
public class ProductRating
{
[BsonElement("Rating")]
public int Rating { get; set; }
}
public class Product
{
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("PLU")]
public int PLU { get; set; }
[BsonElement("Description")]
public string Description { get; set; }
[BsonElement("AverageRating")]
public Double AvgRating { get; set; }
[BsonElement("Ratings")]
public List<ProductRating> Ratings { get; set; }
}
public static class UpdateCart
{
//This function is called locally via: http://localhost:7071/api/ResetDemo
//Its purpose is to clear the MongoOnlineGrocery database, create an index on the PLU field
//and insert a few sample items
[FunctionName("ResetDemo")]
public static async Task<IActionResult> RunGet([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequest req, ILogger log)
{
try
{
var client = new MongoClient(System.Environment.GetEnvironmentVariable("MongoDBAtlasConnectionString"));
var database = client.GetDatabase("MongoOnlineGrocery");
var collection = database.GetCollection<Product>("inventory");
//We could also just drop the collection
await collection.DeleteManyAsync(new BsonDocument { });
await collection.Indexes.DropAllAsync();
//Create an index on the PLU, this will also enforce uniqueness
IndexKeysDefinition<Product> keys = "{ PLU: 1 }";
var indexModel = new CreateIndexModel<Product>(keys);
await collection.Indexes.CreateOneAsync(indexModel);
//Create a default rating of 3 for new products
var MyRating = new ProductRating();
MyRating.Rating = 3;
var DefaultProductRating = new List<ProductRating>();
DefaultProductRating.Add(MyRating);
//Define some sample objects
var Bananas = new Product
{
Id = new ObjectId(),
PLU = 4011,
Description = "Bananas",
Ratings = DefaultProductRating
};
var Apples = new Product
{
Id = new ObjectId(),
PLU = 3283,
Description = "Apples",
Ratings = DefaultProductRating
};
//MongoDB makes it easy to go from object to database with no ORM layer
await collection.InsertOneAsync(Bananas);
await collection.InsertOneAsync(Apples);
}
catch (Exception e)
{
return new BadRequestObjectResult("Error refreshing demo - " + e.Message);
}
return (ActionResult)new OkObjectResult("Refreshed Demo database");
}
[FunctionName("ProductReview")]
public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequest req, ILogger log)
{
int iPLU = 0;
int iRating = 0;
Double iAvg = 0;
if (Int32.TryParse(req.Query["PLU"], out iPLU))
{
iPLU = Int32.Parse(req.Query["PLU"]);
}
else
{
return new BadRequestObjectResult("Please pass a PLU parameter");
}
if (Int32.TryParse(req.Query["Rating"], out iRating))
{
iRating = Int32.Parse(req.Query["Rating"]);
if (iRating<1 || iRating>5)
{
return new BadRequestObjectResult("Rating must be between 1 and 5");
}
}
else
{
return new BadRequestObjectResult("Please pass a Rating parameter");
}
//Create client connection to our MongoDB Atlas database
var client = new MongoClient(System.Environment.GetEnvironmentVariable("MongoDBAtlasConnectionString"));
//Create a session object that is used when leveraging transactions
var session = client.StartSession();
//Create the collection object that represents the "inventory" collection
var collection = session.Client.GetDatabase("MongoOnlineGrocery").GetCollection<Product>("inventory");
//Begin transaction
session.StartTransaction();
//Append the rating to the PLU
var filter = new FilterDefinitionBuilder<Product>().Where(r => r.PLU == iPLU);
//For now, our ratings are just an array of integers, in future we could easily add more metadata
var MyRating = new ProductRating();
MyRating.Rating = iRating;
var update = Builders<Product>.Update.Push<ProductRating>(r => r.Ratings, MyRating);
var options = new UpdateOptions() { IsUpsert = true };
try
{
//Add the rating to our product
await collection.UpdateOneAsync(filter, update, options);
//Calculate the average rating
/* Equivalent Mongo Query Language statement:
*
* db.inventory.aggregate( [
* { $match: { "PLU":4011 }},
* { $unwind: "$Ratings" },
* { $group: { _id: "$_id", AvgRating: { $avg: "$Ratings.Rating" }}}
* ])
*/
//Building out the Group pipeline stage
List<BsonElement> e = new List<BsonElement>();
e.Add(new BsonElement("_id", "$_id"));
e.Add(new BsonElement("AvgRating", new BsonDocument("$avg", "$Ratings.Rating")));
PipelineDefinition < Product,BsonDocument> Pipe = new BsonDocument[]
{
new BsonDocument {{ "$match", new BsonDocument("PLU", iPLU)}},
new BsonDocument {{ "$unwind", new BsonString("$Ratings")}},
new BsonDocument {{ "$group", new BsonDocument(e)}}
};
var AverageRating = await collection.AggregateAsync<BsonDocument>(Pipe);
//We filtered it down to only average a specific PLU
var o = AverageRating.First();
iAvg = o["AvgRating"].AsDouble;
//Now that we calculated the average update the PLU with the latest average
var updateavg = Builders<Product>.Update.Set(r => r.AvgRating, iAvg);
await collection.UpdateOneAsync(filter, updateavg);
//Made it here without error? Let's commit the transaction
session.CommitTransaction();
}
catch (Exception e)
{
session.AbortTransaction();
return new BadRequestObjectResult("Error: " + e.Message);
}
return iRating > 0
? (ActionResult)new OkObjectResult($"Added Rating of {iRating} to {iPLU}. Average rating is {iAvg}")
: new BadRequestObjectResult("Please pass a PLU and Quantity as parameters");
}
}
}
@codermrrob
Copy link

codermrrob commented Jun 5, 2019

Looking at your code here it seems that you are creating a new MongoDb connection for every function call?

I was under the impression that was an incorrect way to handle Mongo api clients, referenced in the docs here:
http://mongodb.github.io/mongo-csharp-driver/2.0/reference/driver/connecting/#re-use

Would it be better to handle the MongoDb CLient in this manner?
https://github.com/Azure/azure-functions-host/wiki/Managing-Connections

Any information here or other comments would be interesting and useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment