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
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
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.
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.
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.
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);