How to export an existing dynamo table schema to json? - amazon-web-services

I'd like to replicate some dynamodb tables, schema only, into my local environment for testing purposes. First I've tried:
aws dynamodb describe-table --table-name Foo > FooTable.json
But it's obvious that the output schema is not compliant to the input schema from the create-table command:
aws dynamodb create-table --cli-input-json file://FooTable.json --endpoint=http://localhost:8000
What I'm trying to avoid is to generate dozens of skeletons with aws dynamodb create-table --generate-cli-skeleton and fill them manually :/
Is there a way to get the table schema in a format that is "useful" for recreation? I find it unbelievable that there are no straightforward way of doing it through the web graphic interface or the standard aws command line - after hearing how "good" was their service.

I just managed to do a complete dump and "restore" using bchew/dynamodump:
git clone git#github.com:bchew/dynamodump.git
Notice the --schemaOnly option in the documentation https://github.com/bchew/dynamodump. Command was:
./dynamodump.py -m backup --schemaOnly --region foo-region --host localhost --srcTable '*' --port 8000 --accessKey fooKey --secretKey barKey
Then you can use the -m restore mode to put the data or schema back into a local dynamodb or wherever desired :)
With that said, I still find it unbelievable how bad is the amazon dynamodb tool-chain. Come on guys.

This takes aws dynamodb describe-table output, and transforms it into the input-format of aws dynamodb create-table --cli-input-json:
AWS_PROFILE=xyz aws dynamodb describe-table --table-name MyTable > mytable_full.json
# Pull out just what is minimally needed for table-creation:
#
# TableName
# KeySchema
# AttributeDefinitions (must only contain attributes used in keys)
# Global/Local Secondary Indexes
# Defaults BillingMode to PAY_PER_REQUEST
# (any provisioning can be set up manually based on load)
jq <mytable_full.json '.Table | {TableName, KeySchema, AttributeDefinitions} + (try {LocalSecondaryIndexes: [ .LocalSecondaryIndexes[] | {IndexName, KeySchema, Projection} ]} // {}) + (try {GlobalSecondaryIndexes: [ .GlobalSecondaryIndexes[] | {IndexName, KeySchema, Projection} ]} // {}) + {BillingMode: "PAY_PER_REQUEST"}' ​>mytable.json
AWS_PROFILE=xyz aws dynamodb create-table --cli-input-json file://mytable.json
You can also paste the json into python (the python dict syntax closely matches json) eg
import boto3
dynamodb = boto3.resource("dynamodb")
tabledef = {
"TableName": "MyTable",
"KeySchema": [
...
}
table = dynamodb.create_table(**tabledef)
print("Table status: ", table.table_status)
References:
https://docs.aws.amazon.com/cli/latest/reference/dynamodb/describe-table.html
https://docs.aws.amazon.com/cli/latest/reference/dynamodb/create-table.html
https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/dynamodb.html#creating-a-new-table

Here is a version using C#, AWS CLI and Newtonsoft JSON on Windows. Start by running this command: -
aws dynamodb describe-table --table-name TheTable --profile SourceAWSCredsProfile > TheTable.json
Pick up the file, deserialize and serialize to the --cli-input-json friendly class: -
TableContainer tableContainer;
string sourceFile = "TheTable.json";
string destFile = "TheTable.cli-input.json";
using (StreamReader file = File.OpenText(sourceFile))
{
JsonSerializer serializer = new JsonSerializer();
tableContainer = (TableContainer)serializer.Deserialize(file, typeof(TableContainer));
}
File.WriteAllText(destFile, JsonConvert.SerializeObject(tableContainer.Table, Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
}
));
Now run this command to import the table definition: -
aws dynamodb create-table --cli-input-json file://TheTable.cli-input.json --profile DestinationAWSCredsProfile
The TableContainer class definition is below. The absence of certain properties cleans out everything that the --cli-input-json parameter doesn't need. You can recreate this class anytime by running: -
aws dynamodb create-table --generate-cli-skeleton
Then copy and paste the output into a new class file, using the very handy Paste Special... Paste JSON as Classes feature in Visual Studio.
public class TableContainer
{
public DynamoTableCLIJSON Table { get; set; }
}
public class DynamoTableCLIJSON
{
public Attributedefinition[] AttributeDefinitions { get; set; }
public string TableName { get; set; }
public Keyschema[] KeySchema { get; set; }
public Localsecondaryindex[] LocalSecondaryIndexes { get; set; }
public Globalsecondaryindex[] GlobalSecondaryIndexes { get; set; }
public string BillingMode { get; set; }
public Provisionedthroughput ProvisionedThroughput { get; set; }
public Streamspecification StreamSpecification { get; set; }
public Ssespecification SSESpecification { get; set; }
public Tag[] Tags { get; set; }
}
public class Provisionedthroughput
{
public int ReadCapacityUnits { get; set; }
public int WriteCapacityUnits { get; set; }
}
public class Streamspecification
{
public bool StreamEnabled { get; set; }
public string StreamViewType { get; set; }
}
public class Ssespecification
{
public bool Enabled { get; set; }
public string SSEType { get; set; }
public string KMSMasterKeyId { get; set; }
}
public class Attributedefinition
{
public string AttributeName { get; set; }
public string AttributeType { get; set; }
}
public class Keyschema
{
public string AttributeName { get; set; }
public string KeyType { get; set; }
}
public class Localsecondaryindex
{
public string IndexName { get; set; }
public Keyschema1[] KeySchema { get; set; }
public Projection Projection { get; set; }
}
public class Projection
{
public string ProjectionType { get; set; }
public string[] NonKeyAttributes { get; set; }
}
public class Keyschema1
{
public string AttributeName { get; set; }
public string KeyType { get; set; }
}
public class Globalsecondaryindex
{
public string IndexName { get; set; }
public Keyschema2[] KeySchema { get; set; }
public Projection1 Projection { get; set; }
public Provisionedthroughput1 ProvisionedThroughput { get; set; }
}
public class Projection1
{
public string ProjectionType { get; set; }
public string[] NonKeyAttributes { get; set; }
}
public class Provisionedthroughput1
{
public int ReadCapacityUnits { get; set; }
public int WriteCapacityUnits { get; set; }
}
public class Keyschema2
{
public string AttributeName { get; set; }
public string KeyType { get; set; }
}
public class Tag
{
public string Key { get; set; }
public string Value { get; set; }
}

Related

Cloud formation custom resource creation fails

I am trying to create a custom resource which points to a lambda function and then invoke it to generate random Priority or my ELB Listener.
Code for Lambda function is as follows.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace aws_listenser_rule_priority_generator {
public class Function {
public async Task<int> FunctionHandler(FunctionParams input, ILambdaContext context) {
AmazonElasticLoadBalancingV2Client elbV2Client = new AmazonElasticLoadBalancingV2Client(RegionEndpoint.EUWest1);
var describeRulesResponse = await elbV2Client.DescribeRulesAsync(new DescribeRulesRequest {
ListenerArn = input.ListenerArn
});
var priority = 0;
var random = new Random();
do {
priority = random.Next(1, 50000);
}
while(describeRulesResponse.Rules.Exists(r => r.Priority == priority.ToString()));
return priority;
}
}
public class FunctionParams {
public string ListenerArn { get; set; }
}
}
I have tested this lambda on AWS console with the following parameters and it returns successfully.
{
"ListenerArn": "arn:aws:elasticloadbalancing:eu-west-1:706137030892:listener/app/Cumulus/dfcf28e0393cbf77/cdfe928b0285d5f0"
}
But as soon as I try to use this with Cloud Formation. The Custom Resource is stuck at creation in progress.
Resources:
ListenerPriority:
Type: Custom::Number
Properties:
ServiceToken: "arn:aws:lambda:eu-west-1:706137030892:function:GenerateListenerPriority"
ListenerArn: "arn:aws:elasticloadbalancing:eu-west-1:706137030892:listener/app/Cumulus/dfcf28e0393cbf77/cdfe928b0285d5f0"
The issue with the previous approach was the data format. When we are working with the Custom Resources, Cloud Formation sends a request in a specified format and response is expected asynchronously at a specified response URL.
I had to make following updates to the code:
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace aws_listenser_rule_priority_generator
{
public class Function
{
public async Task FunctionHandler(CustomResourceRequest<CustomResourceRequestProperties> crfRequest, ILambdaContext context)
{
var jsonResponse = string.Empty;
if(crfRequest.RequestType.ToUpperInvariant() != "CREATE") {
jsonResponse = JsonSerializer.Serialize(new CustomResourceResponse<object> {
Status = "SUCCESS",
Reason = string.Empty,
PhysicalResourceId = Guid.NewGuid().ToString(),
StackId = crfRequest.StackId,
RequestId = crfRequest.RequestId,
LogicalResourceId = crfRequest.LogicalResourceId,
Data = new {
Dummy = "Dummy"
}
});
}
else {
AmazonElasticLoadBalancingV2Client elbV2Client = new AmazonElasticLoadBalancingV2Client(RegionEndpoint.EUWest1);
var describeRulesResponse = await elbV2Client.DescribeRulesAsync(new DescribeRulesRequest {
ListenerArn = crfRequest.ResourceProperties.ListenerArn
});
var priority = 0;
var random = new Random();
do {
priority = random.Next(1, 50000);
}
while(describeRulesResponse.Rules.Exists(r => r.Priority == priority.ToString()));
jsonResponse = JsonSerializer.Serialize(new CustomResourceResponse<object> {
Status = "SUCCESS",
Reason = string.Empty,
PhysicalResourceId = Guid.NewGuid().ToString(),
StackId = crfRequest.StackId,
RequestId = crfRequest.RequestId,
LogicalResourceId = crfRequest.LogicalResourceId,
Data = new {
Priority = priority
}
});
}
var byteArray = Encoding.UTF8.GetBytes(jsonResponse);
var webRequest = WebRequest.Create(crfRequest.ResponseURL) as HttpWebRequest;
webRequest.Method = "PUT";
webRequest.ContentType = string.Empty;
webRequest.ContentLength = byteArray.Length;
using(Stream datastream = webRequest.GetRequestStream()) {
await datastream.WriteAsync(byteArray, 0, byteArray.Length);
}
await webRequest.GetResponseAsync();
}
}
public class CustomResourceRequest<T> where T : ICustomResourceRequestProperties {
public string RequestType { get; set; }
public string ResponseURL { get; set; }
public string StackId { get; set; }
public string RequestId { get; set; }
public string ResourceType { get; set; }
public string LogicalResourceId{ get; set; }
public string PhysicalResourceId { get; set; }
public T ResourceProperties { get; set; }
public T OldResourceProperties { get; set; }
}
public class CustomResourceResponse<T> {
public string Status { get; set; }
public string Reason { get; set; }
public string PhysicalResourceId { get; set; }
public string StackId { get; set; }
public string RequestId { get; set; }
public string LogicalResourceId { get; set; }
public bool NoEcho { get; set; }
public T Data { get; set; }
}
public interface ICustomResourceRequestProperties {
public string ServiceToken { get; set; }
}
public class CustomResourceRequestProperties : ICustomResourceRequestProperties
{
string ICustomResourceRequestProperties.ServiceToken { get; set; }
public string ListenerArn { get; set; }
}
}
The above code expects everything to work without any issues and there are no try catch blocks. Best practice would be to have try catch blocks and send a failure response.
Following URLs can be referred for more details:
Custom resources
Custom resource request objects
Custom resource response objects
Using AWS Lambda with AWS CloudFormation

How to retrieve list from DB to front-end with AutoMapper

After changing the mapping to Automapper, only an empty list is sent through the endpoint.
Initially I had an endpoint that retrieved all employees with info including a list with every course each employee had taken. This was with manual mapping between entities & Dto.
//From startup.cs in Configure
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<Employee, Models.EmployeeCoursesDto>();
cfg.CreateMap<Employee, Models.EmployeeDto>();
cfg.CreateMap<EmployeeCourses, Models.EmployeeCoursesDto>();
});
//From Employee entity
public class Employee
{
[Key]
//Gen new Id key in DB when object created
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(50)]
public string Title { get; set; }
public ICollection<EmployeeCourses> EmployeeCourses { get; set; }
= new List<EmployeeCourses>();
}
}
//From employee Dto
public class EmployeeDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public ICollection<EmployeeCoursesDto> EmployeeCourses { get; set; }
= new List<EmployeeCoursesDto>();
}
}
//Endpoint in controller
[HttpGet()]
public IActionResult GetAllEmployees()
{
var employeeEntities = _employeeInfoRepository.GetEmployees();
var results = Mapper.Map<IEnumerable<EmployeeDto>>(employeeEntities);
return Ok(results);
}
//From Irepository
IEnumerable<Employee> GetEmployees();
//From repository
public IEnumerable<Employee> GetEmployees()
{
return _context.Employees.OrderBy(c => c.Name).ToList();
}
I expected output all employees with all datafileds, including their list of courses.
The output is all fields with data, except the list of courses which is "0" when running with a breakpoint, and in Postman it shows as only:
"id": 2,
"name": "Test Person",
"title": "Bus Driver",
"numberOfCourses": 0,
"employeeCourses": [],
"totalAchievedHoursAuditor": 0,
"totalAchievedHoursAccountant": 51,
"courseBalanceAccountant": null,
"courseBalanceAuditor": null
However, if I try another endpoint only for retrieving a specific course, or a list of courses, the data show correctly. Seems there are an issue with mapping the employees & courses at the same time?
I found the error, not Automapper, but my Linq statement:
return _context.Employees.Include(c => c.EmployeeCourses).ToList();
Please close this thread. Thanks for the reply Lucian Bargaoanu & have a great weekend.

EF Core - Fluent API many-to-many self reference

I am attempting to configure 2 classes fluently.
public class Company
{
[Key]
public int Id {get; set; }
public string Name { get; set; }
public List<CompanyOwnership> OwnedBy { get; set; }
}
public class CompanyOwnership
{
public static void Configure(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CompanyOwnership>()
.HasOne(cpo => cpo.OwnedCompany)
.WithMany(cp => cp.OwnedBy)
.HasForeignKey(cpo => cpo.OwnedCompanyId);
modelBuilder.Entity<CompanyOwnership>()
.HasOne(cpo => cpo.OwningCompany)
.WithMany()
.HasForeignKey(cpo => cpo.OwningCompanyId);
}
[Key]
public int Id {get; set; }
public int OwnedCompanyId { get; set; }
public Company OwnedCompany { get; set; }
public int OwningCompanyId { get; set; }
public Company OwningCompany { get; set; }
public decimal Percentage { get; set; }
}
The above code will result in an error:
InvalidOperationException: Unable to determine the relationship
represented by navigation property 'Company.OwnedBy' of type
'List<CompanyOwnership>'. Either manually configure the relationship,
or ignore this property using the '[NotMapped]' attribute or by using
'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
Could I get some input about why the above setup is not enough?
Thank you,
Nvm,
it turned out that I forgot to call Configure(...).
It is working fine now.

How do I use c# List<string> to store multiple file paths in ASP.NET MVC 4

I am working on a small project with ASP.NET MVC 4 and .net framework 4.5. I have a class RecReservation to deal with user reservations. A user may make a reservation at first and upload 2-3 files later, the files have to be related to a certain reservation. At first I defined a string type in RecReservation to store file path, it worked, but only one file can be related to a reservation in this way, I need a solution to handle multiple file paths.
So I defined a List<string> _recFilePath in my RecReservation class to store the file paths. Each time a file is uploaded, the path of which should be appended to _recFilePath. The class looks like this:
public class RecReservation
{
[HiddenInput(DisplayValue = false)]
public int RecReservationID { get; set; }
public String ProjectName { get; set; }
[Required]
public string ProjectManager { get; set; }
public String Department { get; set; }
[DataType(DataType.Date)]
public DateTime ReservationDate { get; set; }
public String ReservationTime { get; set; }
[Required]
public string CourseName { get; set; }
[Required]
public string InstructorName { get; set; }
[Required]
public string PhoneNumber { get; set; }
[DataType(DataType.MultilineText)]
public string Description { get; set; }
private List<string> _recFilePath = new List<string>();
public List<string> RecFilePath
{
get
{ return _recFilePath; }
}
public void AddRecFilePath(string newpath)
{
_recFilePath.Add(newpath);
}
}
In the database, I defined a nvarchar(500) data type for _recFilePath.
In the controller, I defined a pair of RecUpload overloads to deal with file uploads:
public ViewResult RecUpload(int recReservationID)
{
RecReservation recReservation = repository.RecReservations.FirstOrDefault(p => p.RecReservationID == recReservationID);
return View(recReservation);
}
[HttpPost]
public ActionResult RecUpload(HttpPostedFileBase file, RecReservation recReservation)
{
string fileName = Path.GetFileName(file.FileName);
string path = Path.Combine(Server.MapPath("~/Files/RecUploads"), fileName);
file.SaveAs(path);
repository.AddFilePathToRec(recReservation, path);
TempData["message"] = string.Format("{0} has been uploaded", fileName);
return RedirectToAction("AdminRecList");
}
This is the implementation of AddFilePathToRec
public void AddFilePathToRec(RecReservation recReservation, string newFilePath)
{
RecReservation dbEntry = context.RecReservations.Find(recReservation.RecReservationID);
if (dbEntry != null)
{
dbEntry.AddRecFilePath(newFilePath);
}
context.SaveChanges();
}
Now that I have tested the implementation, files upload works fine, but no path is added to _recFilePath field in my database. So what is the reason for this? Help is appreciated! Thank you in advance.

Microsoft Moles : circular reference error?

I've the following Entity Framework entities:
public class Country
{
public long Id { get; set; }
public string Code { get; set; }
public virtual ICollection<Person> Persons { get; set; }
}
public class Person
{
public long Id { get; set; }
public long? Country_Id { get; set; }
public Country HomeCountry { get; set; }
}
Moles has generated MPerson and MCountry stub classes.
Now I do want to stub the set of the Country_Id:
MPerson.AllInstances.Country_IdSetNullableOfInt64 = (Person instance, long? id) =>
{
// Do something
// Set the Country_Id to the provided id
// This will trigger this same method again and again. How to avoid this ?
instance.Country_Id = id;
};
This post gives the answer:
MolesContext.ExecuteWithoutMoles(() => instance.Country_Id = id);