AWS: AppSync with Lambda Resolver for MongoDB using DotNet core


In this article, we will learn how to create an AppSync API with the Lambda function. I will show you how a lambda function is written using the Dotnet core that performs business logic based on invoking graphical field operation. AppSync enables us to use AWS Lambda to resolve any graphing field.

Technology

Framework- Visual studio 2019
DotNet core and backend database MongoDB

Prerequisites

You should have the basic knowledge of .Net Core with C#, Visual Studio, and NoSQL database understanding will be additional.

This article divided into three parts.
Part 1- Lambda Function development using DotNet core.
Part 2- Deployment on AWS
Part 3 – AppSync API

Lambda Function development using DotNet core

STEP 1- First, you install AWS Toolkit for Visual Studio. The AWS Toolkit is an extension for Microsoft Visual Studio makes it more accessible for developers to develop, debug, and deploy .NET applications using Amazon Web Services. It provides the templates that able you to get started faster and be more productive when building AWS applications. AWS Toolkit supports to VS 2013- VS 2015 and VS 2017 - VS 2019 (For both operating system windows and MAC). Follow the Link(https://aws.amazon.com/visualstudio/) and download/install it According to your convenience. If your MongoDB is installed and working, then good else you can follow the link How to Install MongoDB on windows.


STEP 2- Open Visual Studio and Click on the new project 

Search with keyword "AWS Lambda Function" select the project marks as below 

Now, Configure you're a project such as AWSLambdaResolver, click on the create button it will create a project at your path location.


STEP 3- Add Startup class and replace with the code given below.
Note: It may be some assembly error arises in code, to remove this issue you have to add this DLL from NuGetPackge.

You may have to install the DLL listed below
1- Microsoft.AspNetCore
2- Microsoft.AspNetCore.Mvc
3- Microsoft.AspNetCore.Identity -- if you want some security on your function
4- Microsoft.EntityFrameworkCore-- if you are using entity framework

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using AWSLambdaResolver.Models;

namespace AWSLambdaResolver
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<DatabaseSettings>(
            Configuration.GetSection(nameof(DatabaseSettings)));
            services.AddSingleton<IDatabaseSettings>(sp =>
                sp.GetRequiredService<IOptions<DatabaseSettings>>().Value);
            services.AddSingleton<BookService>();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}


Step 4- In this step, I will create the entity, first, create Models directory in the project root. Add the Book class and copy-paste the following code.

You have to install the package of MongoDB.Driver using NuGetPackges

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Text;

namespace AWSLambdaResolver.Models
{
    public class Book
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string _id { get; set; }

        //[BsonElement("Name")]
        public string BookName { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
        public string Author { get; set; }
        public string appsyncResponse { set; get; }
    }
    
}


And add another class MyRequest-- this object use in function which is will be mapped from AppSync

using System;
using System.Collections.Generic;
using System.Text;

namespace AWSLambdaResolver.Models
{
    public class MyRequest
    {
        public string RequestType { set; get; }
        public Book book { set; get; }
    }
}


Step 5- In this step we will add a configuration model, where we will provide the database configuration(appsettings.json).
As we know the DotNet Core follows the Dependency Injection architecture, we can push the database credential from the Lambda Function or also can configure the database in the startup.cs class.
{
  "DatabaseSettings": {
    "BooksCollectionName": "Books",
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "BookstoreDb"
  },
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  }
}


Step 6 - In this step, we will add a DatabaseSettings.cs file to the Models directory with the following code. The preceding DatabaseSettings class is used to store the appsettings.json file's DatabaseSettings property values. The JSON and C# property names are named identically to ease the mapping process.

using System;
using System.Collections.Generic;
using System.Text;

namespace AWSLambdaResolver.Models
{
    public class  DatabaseSettings : IDatabaseSettings
    {
        public string BooksCollectionName { get; set; }
        public string ConnectionString { get; set; }
        public string DatabaseName { get; set; }
    }

    public interface IDatabaseSettings
    {
        string BooksCollectionName { get; set; }
        string ConnectionString { get; set; }
        string DatabaseName { get; set; }
    }
}


Step 7- In this step, we will create a CRUD operation service in the Models folder to add the Services.cs class and copy-paste the following code. In the preceding code, an IDatabaseSettings instance is retrieved from Dependency Injection via constructor injection.

using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Text;

namespace AWSLambdaResolver.Models
{
    public class BookService
    {
        private readonly IMongoCollection<Book> _books;

        public BookService(IDatabaseSettings settings)
        {
            var client = new MongoClient(settings.ConnectionString);
            var database = client.GetDatabase(settings.DatabaseName);

            _books = database.GetCollection<Book>(settings.BooksCollectionName);
        }

        public List<Book> Get() =>
            _books.Find(x => true).ToList();

        public Book Get(string id) =>
            _books.Find<Book>(x => x._id == id).FirstOrDefault();

        public Book Create(Book x)
        {
            _books.InsertOne(x);
            return x;
        }

        public void Update(string id, Book bk) =>
            _books.ReplaceOne(x => x._id == id, bk);

        public void Remove(Book bk) =>
            _books.DeleteOne(x => x._id == bk._id);

        public void Remove(string id) =>
            _books.DeleteOne(x => x._id == id);
    }
}


Step 8- now we will understand the Startup.ConfigureServices, first, we will talk about the following code section.


            services.Configure<DatabaseSettings>(
            Configuration.GetSection(nameof(DatabaseSettings)));
            services.AddSingleton<IDatabaseSettings>(sp =>
                sp.GetRequiredService<IOptions<DatabaseSettings>>().Value);


The configuration instance to which the appsettings.json file's DatabaseSettings section binds is registered in the Dependency Injection container. For example, a DatabaseSettings object's ConnectionString property is populated with the DatabaseSettings:ConnectionString property in appsettings.json.
The IDatabaseSettings interface is registered in Dependency Injection with a singleton service lifetime. When injected, the interface instance resolves to a DatabaseSettings object.

services.AddSingleton<BookService>();

In the above line of code the BookService class is registered with Dependency Injection to support constructor injection in consuming classes

Step 9- Create an object class to consume the CRUD service operation (Business Layer coding or if you are using the API controller direct you can code on in the controller)

using System;
using System.Collections.Generic;
using System.Text;

namespace AWSLambdaResolver.Models
{
    public class CrudOperation
    {
        private readonly BookService _bookService;

        public CrudOperation(BookService bookService)
        {
            _bookService = bookService;
        }
        public List<Book> Get() => _bookService.Get();
        public Book Get(string id)
        {
            var x = _bookService.Get(id);

            if (x == null)
            {
                return null;
            }

            return x;
        }
        public Book Create(Book x)
        {
            _bookService.Create(x);

            return new Book() { _id = x._id.ToString() };
        }


        public string Update(string id, Book bk)
        {
            var x = _bookService.Get(id);

            if (x == null)
            {
                return "no record found to update";
            }

            _bookService.Update(id, bk);

            return "Updated";
        }

        public string Delete(string id)
        {
            var x = _bookService.Get(id);

            if (x == null)
            {
                return "No records found to delete";
            }

            _bookService.Remove(x._id);

            return "Records Deleted";
        }
    }
}



Step 10- In this step we will work on Lambda Function method where we will implement the CRUD operation that will call the different service class methods on a request basis.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using AWSLambdaResolver.Models;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AWSLambdaResolver
{
    public class Function
    {
       
        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public object FunctionHandler(MyRequest input, ILambdaContext context)
        {
            IDatabaseSettings bs = new DatabaseSettings();
            bs.BooksCollectionName = "Books";
            bs.ConnectionString = "your db connection string";
            bs.DatabaseName = "your database name";
            BookService mybookservice = new BookService(bs);
            CrudOperation crd = new CrudOperation(mybookservice);
            switch (input.RequestType)
            {
                case "AddBook":
                    {
                        input.book.appsyncResponse = "Book Added";
                        return crd.Create(input.book);
                    }
                case "Update":
                    {
                        input.book.appsyncResponse = "Records Updated";
                        input.book._id = crd.Update(input.book._id, input.book);
                        return input.book;
                    }
                case "Delete":
                    {
                        input.book.appsyncResponse = "Record Deleted";
                        input.book._id = crd.Delete(input.book._id);
                        return input.book;
                    }
                case "ReadById":
                    {
                        return crd.Get(input.book._id);
                    }
                case "ReadAll":
                    {
                        return crd.Get();
                    }
                default:
                    {
                        input.book.appsyncResponse = "No Action";
                        return input.book;
                    }
            }
        }
    }
}


Step 11- Build your code now, if build successfully then it is ready to deploy on the AWS Lambda Function. To deploy on the AWS Lambda function please follow the following steps.

AWS Lambda Function deployment 

Step 1- Create AWS account using the link console.aws.amazon.com, it is free for one year (but have to use carefully some services are chargeable)

Step 2- In this step, we will find out needed stuff for deployment  
Right-Click on ”AWSLambdaResolver” àPublish to AWS Lambda

Click on “Publish to AWS Lambda”, the following window will open as below

Account Profile to use: Here you will set up the account, once you click on the icon the following the window will open.
The following option will be shown on window

1-   Profile Name: This is the required field which will be set up in your visual studio that will be used for further. This is a profile name of “default” allows the SDK to find credentials when no explicit profile name is specified in your code or application configuration setting.
2-   Storage Location: Using the user credential file, the profile AWS credential will be stored in the <home directory> \.aws\credential file. The profile accessible to all AWS SDKs and tools.
3-   Access Key ID: Access Key ID provided by AWS
4-   Secret Access Key: Provided By AWS
5-   Account Number: Provided by AWS
6-   Account Type: Choose as default

Region: Select the region where you want to deploy the Lambda Function, if you are working on the business purpose then you should choose the option as per your pricing ability.
Function Details:
Function Name: Set as the default
Configuration: Configuration of have two option,
·         Release
·         Debug
Choose as per your requirement.
Assembly Name: Set as default
Account Number: You can get an account number from your AWS account
Account Type Name: Set as default
Method Name: Set as default

If all the above options filled/chosen properly then, click on the Next button. Action on the next window is given below.

Permission
Role Name: Select an IAM Role to provide AWS credentials to our lambda function allowing access to AWS Service like S3.

Execution
Memory: By default its 256, put the memory size as per your requirement.
Timeout: Set time out in seconds.

VPC (Virtual Private Cloud)
VPC Subnet: Set the PVC to run your function on the private network
Security Groups: You should always aim to restrict access with your security group to help maintain restriction of access at the protocol and port level.
For the development you can leave this option.
 Debugging and Error Handling
This option is optional where you can set up the error handling for the Lambda Function.
Environment
Choose as default

Now, finally, click on the Upload button, and wait for a while and a new window will open in visual studio for the testing the function.

You can check your account where you can see the Lambda Function has created. Form AWS Lambda Function you can test, it is working or not.

If our Lambda Function is working fine, it means we are ready to work for the AppSync Resolver.

AppSync API

To create AppSync please follow the below steps
Step 1- Click, Servicesàin search type AppSync à AWS AppSync
The following window will open, click on the Create API

Choose the option Build From Scratch
Step 2- Create Resource: Configure API Name and click to create a button.
Step 3- Now, your API is created.

Step 4- In this step, we will configure Data Source.
Click on the create button and configure the Data Source from Lambda Function. Fill the required input as below image and click on the Create Button

Your Data Source Created
Step 5- Now, we will configure the schema.
Copy-paste the following code in the schema section and click on the Save Schema button

type Mutation {
                AddBook(
                                _id: ID!,
                                BookName: String!,
                                Price: Int,
                                Category: String,
                                Author: String
                ): Post!
}

type Post {
                _id: ID!
                BookName: String!
                Price: Int
                Category: String
                Author: String
                appsyncResponse: String
}

type Query {
                UpdateBook(
                                _id: ID!,
                                BookName: String!,
                                Price: Int,
                                Category: String,
                                Author: String
                ): Post
                DeleteBook(_id: ID!): Post
                ReadByID(_id: ID!): Post
                ReadAll: [Post]
}

schema {
                query: Query
                mutation: Mutation
}

Step 6- In this step, we configure Resolver one by for the AddBook, UpdateBook, ReadByID, and ReadAll.
Resolver means, we are mapping our GraphQL schema to Lambda Function input and output results.
Configure Resolver for AddBook method
Click on Attached as marked in the image.

After Click you will find the following window.

Data Source Name: Choose the Data Source Name you have created in Step 4.
Configure the request mapping template: Copy past the following code.

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "RequestType": "AddBook",
        "book":  $utils.toJson($context.arguments)
    }
}

Here, the payload is the body where you will pass the same structure as you passed in the testing of lambda. $context.arguments  will contains the mutation schema of GraphQL
Post {
                _id: ID!
                BookName: String!
                Price: Int
                Category: String
                Author: String
                appsyncResponse: String
}
$utils.tojson() function convert the GrphQL input into JSON.
You can see it is the same structure of request we earlier test in Lambda Function.
Same as AddBook Resolver, we will configure other resolvers also.
Configure Resolver for UpdateBook
Copy and paste the following code

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "RequestType": "Update",
        "book":  $utils.toJson($context.arguments)
    }
}


Configure Resolver for DeleteBook
Copy and paste the following code

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "RequestType": "Delete",
        "book":  $utils.toJson($context.arguments)
    }
}


Configure Resolver for ReadByID
Copy-paste the following code

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "RequestType": "ReadById",
        "book":  $utils.toJson($context.arguments)
    }
}


Configure Resolver for ReadAll
Copy-paste the following code

{
    "version": "2017-02-28",
    "operation": "Invoke",
    "payload": {
        "RequestType": "ReadAll"
    }
}


You’re all setup done. Now we will learn GraphQL query

GraprhQL Query

Add Query

Add Book using GraphQL query. I will use the AWS console to run the GraphQL query.

mutation addPost {
    AddBook(
        _id: "58f5e3ea03102d23ec654389"
        BookName: "My test Book"
        Price: 120
        Category: "Book"
        Author: "Dilip singh"
    ) {
        _id    
    }
}

Above Query will return the _id of creating a record.

Result:
{
  "data": {
    "AddBook": {
      "_id": "58f5e3ea03102d23ec654389"
    }
  }
}


Update Query

I am updating the above record.

query UpdateBook{
  UpdateBook(
    _id :"58f5e3ea03102d23ec654389"
     BookName:"My update"
    Price:12
    Category:"Book1"
    Author:"Dilip Singh"
  ){
    _id
  }
}


Result:

{
  "data": {
    "UpdateBook": {
      "_id": "Updated"
    }
  }
}

Read Query

Ready By ID: The fetch query I written below

query ReadById
{
  ReadByID(_id:"58f5e3ea03102d23ec744389")
  {
    _id
    BookName
    Price
    Category
    Author
  }
}

Result:

{
  "data": {
    "ReadByID": {
      "_id": "58f5e3ea03102d23ec744389",
      "BookName": "My Book 2",
      "Price": 120,
      "Category": "Test 2",
      "Author": "Dilip singh"
    }
  }
}

Read All Query
Run the following GraphQL Query

query ReadAll
{
  ReadAll {
    _id
    BookName
    Price
    Category
    Author
  }
}

Result:

{
  "data": {
    "ReadAll": [
      {
        "_id": "58f5e3ea03102d23ec744380",
        "BookName": "My Book",
        "Price": 12,
        "Category": "Test",
        "Author": "Dilip"
      },
      {
        "_id": "58f5e3ea03102d23ec744355",
        "BookName": "My Book 2",
        "Price": 120,
        "Category": "Test 2",
        "Author": "Dilip singh"
      }
    ]
  }
}


more accessibleHope this article helped you achieve your goals and a good education. Please subscribe to my site for new updates and keep in touch through mail and comments.

No comments:

Post a Comment

Please do not enter any spam link in the comment box.

Related Posts

What is the Use of isNaN Function in JavaScript? A Comprehensive Explanation for Effective Input Validation

In the world of JavaScript, input validation is a critical aspect of ensuring that user-provided data is processed correctly. One indispensa...