├── logo.jpg ├── src ├── EasyDynamo │ ├── Attributes │ │ ├── DynamoDbSetAttribute.cs │ │ └── IgnoreAutoResolvingAttribute.cs │ ├── Abstractions │ │ ├── ITableDropper.cs │ │ ├── IEntityConfigurationMetadata.cs │ │ ├── ITableCreator.cs │ │ ├── IPropertyTypeBuilder.cs │ │ ├── IDependencyResolver.cs │ │ ├── IEntityTypeConfiguration.cs │ │ ├── IIndexConfigurationFactory.cs │ │ ├── IIndexFactory.cs │ │ ├── IDynamoContextOptionsProvider.cs │ │ ├── IGenericEntityConfiguration.cs │ │ ├── IEntityValidator.cs │ │ ├── IAttributeDefinitionFactory.cs │ │ ├── ITableNameExtractor.cs │ │ ├── IIndexExtractor.cs │ │ ├── IPrimaryKeyExtractor.cs │ │ ├── IEntityConfiguration.cs │ │ ├── IEntityConfigurationProvider.cs │ │ ├── IDynamoContextOptions.cs │ │ ├── IEntityTypeBuilder.cs │ │ └── IDynamoDbSet.cs │ ├── Extensions │ │ ├── TypeExtensions.cs │ │ ├── HttpStatusCodeExtentions.cs │ │ ├── PropertyInfoExtensions.cs │ │ ├── CollectionExtensions.cs │ │ ├── DynamoDBContextExtensions.cs │ │ ├── ExpressionExtensions.cs │ │ ├── ObjectExtensions.cs │ │ └── DependencyInjection │ │ │ └── ServiceCollectionExtensions.cs │ ├── Tools │ │ ├── Instantiator.cs │ │ ├── TableDropper.cs │ │ ├── Resolvers │ │ │ └── ServiceProviderDependencyResolver.cs │ │ ├── Validators │ │ │ ├── InputValidator.cs │ │ │ └── EntityValidator.cs │ │ ├── Providers │ │ │ ├── DynamoContextOptionsProvider.cs │ │ │ └── EntityConfigurationProvider.cs │ │ ├── Extractors │ │ │ ├── PrimaryKeyExtractor.cs │ │ │ ├── TableNameExtractor.cs │ │ │ └── IndexExtractor.cs │ │ └── TableCreator.cs │ ├── Config │ │ ├── PropertyConfiguration.cs │ │ ├── Constants.cs │ │ ├── ExceptionMessage.cs │ │ ├── GlobalSecondaryIndexConfiguration.cs │ │ ├── EntityConfiguration.cs │ │ └── DynamoContextOptions.cs │ ├── Core │ │ ├── PaginationResponse.cs │ │ ├── DatabaseFacade.cs │ │ └── DynamoContext.cs │ ├── Exceptions │ │ ├── RequestFailedException.cs │ │ ├── EntityNotFoundException.cs │ │ ├── CreateTableFailedException.cs │ │ ├── DeleteTableFailedException.cs │ │ ├── UpdateTableFailedException.cs │ │ ├── EntityAlreadyExistException.cs │ │ ├── EntityValidationFailedException.cs │ │ ├── DynamoDbIndexMissingException.cs │ │ └── DynamoContextConfigurationException.cs │ ├── Builders │ │ ├── PropertyTypeBuilder.cs │ │ ├── DynamoContextOptionsBuilder.cs │ │ ├── ModelBuilder.cs │ │ └── EntityTypeBuilder.cs │ ├── EasyDynamo.csproj │ └── Factories │ │ ├── IndexFactory.cs │ │ ├── AttributeDefinitionFactory.cs │ │ └── IndexConfigurationFactory.cs ├── EasyDynamo.Tests │ ├── Fakes │ │ ├── EntityConfigurationFake.cs │ │ ├── PropertyConfigurationFake.cs │ │ ├── DynamoContextOptionsBuilderFake.cs │ │ ├── PropertyTypeBuilderFake.cs │ │ ├── EntityTypeBuilderFake.cs │ │ ├── ModelBuilderFake.cs │ │ ├── EntityTypeConfugurationFake.cs │ │ ├── FakeEntity.cs │ │ ├── DynamoContextOptionsFake.cs │ │ └── FakeDynamoContext.cs │ ├── TestRetrier.cs │ ├── EasyDynamo.Tests.csproj │ ├── Builders │ │ ├── PropertyTypeBuilderTests.cs │ │ ├── ModelBuilderTests.cs │ │ └── DynamoContextOptionsBuilderTests.cs │ ├── Tools │ │ ├── TableDropperTests.cs │ │ └── Validators │ │ │ └── EntityValidatorTests.cs │ ├── Factories │ │ ├── IndexConfigurationFactoryTests.cs │ │ ├── AttributeDefinitionFactoryTests.cs │ │ └── IndexFactoryTests.cs │ ├── Config │ │ └── DynamoContextOptionsTests.cs │ └── Extensions │ │ └── DependencyInjection │ │ └── ServiceCollectionExtensionsTests.cs └── EasyDynamo.sln ├── LICENSE.txt ├── doc ├── main.md ├── configure-local-client.md ├── code-first.md ├── configure-models.md ├── configure-access.md └── dynamo-context.md └── .gitignore /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msotiroff/EasyDynamo/HEAD/logo.jpg -------------------------------------------------------------------------------- /src/EasyDynamo/Attributes/DynamoDbSetAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Attributes 4 | { 5 | internal class DynamoDbSetAttribute : Attribute 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/ITableDropper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EasyDynamo.Abstractions 5 | { 6 | public interface ITableDropper 7 | { 8 | Task DropTableAsync(string tableName); 9 | } 10 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityConfigurationMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IEntityConfigurationMetadata 6 | { 7 | Type EntityType { get; } 8 | 9 | Type ContextType { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/ITableCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EasyDynamo.Abstractions 5 | { 6 | public interface ITableCreator 7 | { 8 | Task CreateTableAsync(Type contextType, Type entityType, string tableName); 9 | } 10 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Attributes/IgnoreAutoResolvingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 6 | public class IgnoreAutoResolvingAttribute : Attribute 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IPropertyTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IPropertyTypeBuilder 6 | { 7 | IPropertyTypeBuilder HasDefaultValue(object defaultValue); 8 | 9 | IPropertyTypeBuilder IsRequired(bool required = true); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/EntityConfigurationFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | 3 | namespace EasyDynamo.Tests.Fakes 4 | { 5 | public class EntityConfigurationFake : EntityConfiguration 6 | { 7 | public EntityConfigurationFake() 8 | : base() 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Extensions 4 | { 5 | public static class TypeExtensions 6 | { 7 | public static object GetDefaultValue(this Type type) 8 | { 9 | return type.IsValueType ? Activator.CreateInstance(type) : null; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IDependencyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IDependencyResolver 6 | { 7 | TDependency GetDependency(); 8 | 9 | object GetDependency(Type dependencyType); 10 | 11 | object TryGetDependency(Type dependencyType); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Core; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IEntityTypeConfiguration 6 | where TContext : DynamoContext 7 | where TEntity : class 8 | { 9 | void Configure(IEntityTypeBuilder builder); 10 | } 11 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Instantiator.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace EasyDynamo.Tools 4 | { 5 | internal static class Instantiator 6 | { 7 | internal static T GetConstructorlessInstance() where T : class 8 | { 9 | return (T)FormatterServices.GetUninitializedObject(typeof(T)); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/HttpStatusCodeExtentions.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace EasyDynamo.Extensions 4 | { 5 | public static class HttpStatusCodeExtentions 6 | { 7 | public static bool IsSuccessful(this HttpStatusCode statusCode) 8 | { 9 | return (int)statusCode >= 200 && (int)statusCode < 300; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/PropertyConfigurationFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | 3 | namespace EasyDynamo.Tests.Fakes 4 | { 5 | public class PropertyConfigurationFake : PropertyConfiguration 6 | { 7 | protected internal PropertyConfigurationFake(string memberName) 8 | : base(memberName) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IIndexConfigurationFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using EasyDynamo.Config; 4 | 5 | namespace EasyDynamo.Abstractions 6 | { 7 | public interface IIndexConfigurationFactory 8 | { 9 | IEnumerable CreateIndexConfigByAttributes( 10 | Type entityType); 11 | } 12 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IIndexFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Amazon.DynamoDBv2.Model; 3 | using EasyDynamo.Config; 4 | 5 | namespace EasyDynamo.Abstractions 6 | { 7 | public interface IIndexFactory 8 | { 9 | IEnumerable CreateRequestIndexes( 10 | IEnumerable indexes); 11 | } 12 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/PropertyInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace EasyDynamo.Extensions 4 | { 5 | public static class PropertyInfoExtensions 6 | { 7 | public static void SetDefaultValue(this PropertyInfo propertyInfo, object instance) 8 | { 9 | propertyInfo.SetValue(instance, propertyInfo.PropertyType.GetDefaultValue()); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/DynamoContextOptionsBuilderFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Builders; 3 | 4 | namespace EasyDynamo.Tests.Fakes 5 | { 6 | public class DynamoContextOptionsBuilderFake : DynamoContextOptionsBuilder 7 | { 8 | protected internal DynamoContextOptionsBuilderFake(IDynamoContextOptions options) 9 | : base(options) 10 | { 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/PropertyTypeBuilderFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Builders; 2 | using EasyDynamo.Config; 3 | 4 | namespace EasyDynamo.Tests.Fakes 5 | { 6 | public class PropertyTypeBuilderFake : PropertyTypeBuilder 7 | { 8 | protected internal PropertyTypeBuilderFake( 9 | PropertyConfiguration configuration) 10 | : base(configuration) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IDynamoContextOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Core; 2 | using System; 3 | 4 | namespace EasyDynamo.Abstractions 5 | { 6 | public interface IDynamoContextOptionsProvider 7 | { 8 | IDynamoContextOptions GetContextOptions() where TContext : DynamoContext; 9 | 10 | IDynamoContextOptions GetContextOptions(Type contextType); 11 | 12 | IDynamoContextOptions TryGetContextOptions() where TContext : DynamoContext; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IGenericEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | 6 | namespace EasyDynamo.Abstractions 7 | { 8 | public interface IEntityConfiguration : IEntityConfiguration 9 | where TEntity : class 10 | { 11 | Expression> HashKeyMemberExpression { get; } 12 | 13 | ICollection> Properties { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityValidator.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace EasyDynamo.Abstractions 7 | { 8 | public interface IEntityValidator 9 | { 10 | IEnumerable GetValidationResults( 11 | Type contextType, TEntity entity) where TEntity : class; 12 | 13 | void Validate(Type contextType, TEntity entity) where TEntity : class; 14 | } 15 | } -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/EntityTypeBuilderFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Builders; 3 | using EasyDynamo.Config; 4 | 5 | namespace EasyDynamo.Tests.Fakes 6 | { 7 | public class EntityTypeBuilderFake : EntityTypeBuilder 8 | { 9 | protected internal EntityTypeBuilderFake( 10 | EntityConfiguration entityConfig, 11 | IDynamoContextOptions contextOptions) : base(entityConfig, contextOptions) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IAttributeDefinitionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Amazon.DynamoDBv2; 3 | using Amazon.DynamoDBv2.Model; 4 | using EasyDynamo.Config; 5 | 6 | namespace EasyDynamo.Abstractions 7 | { 8 | public interface IAttributeDefinitionFactory 9 | { 10 | IEnumerable CreateAttributeDefinitions( 11 | string primaryKeyMemberName, 12 | ScalarAttributeType primaryKeyMemberAttributeType, 13 | IEnumerable gsisConfiguration); 14 | } 15 | } -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/ModelBuilderFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Builders; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace EasyDynamo.Tests.Fakes 7 | { 8 | public class ModelBuilderFake : ModelBuilder 9 | { 10 | protected internal ModelBuilderFake(IDynamoContextOptions contextOptions) 11 | : base(contextOptions) 12 | { 13 | } 14 | 15 | public IDictionary EntityConfigurationsFromBase 16 | => base.EntityConfigurations; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/ITableNameExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DocumentModel; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace EasyDynamo.Abstractions 6 | { 7 | public interface ITableNameExtractor 8 | { 9 | string ExtractTableName( 10 | IDynamoContextOptions options, 11 | IEntityConfiguration entityConfiguration, 12 | Table tableInfo = null) 13 | where TEntity : class, new(); 14 | 15 | Task> ExtractAllTableNamesAsync(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Config/PropertyConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Config 4 | { 5 | public class PropertyConfiguration 6 | { 7 | public PropertyConfiguration(string memberName) 8 | { 9 | this.MemberName = memberName; 10 | this.PropertyType = typeof(TEntity).GetProperty(memberName).PropertyType; 11 | } 12 | 13 | public string MemberName { get; } 14 | 15 | public Type PropertyType { get; } 16 | 17 | public object DefaultValue { get; set; } 18 | 19 | public bool IsRequired { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/EasyDynamo/Core/PaginationResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace EasyDynamo.Core 4 | { 5 | public class PaginationResponse where TEntity : class, new() 6 | { 7 | /// 8 | /// Gets the next set of items. 9 | /// 10 | public IEnumerable NextResultSet { get; internal set; } 11 | 12 | /// 13 | /// Gets the current pagination token. 14 | /// Return it to the next call to retrieve the next set of items. 15 | /// 16 | public string PaginationToken { get; internal set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IIndexExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DocumentModel; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IIndexExtractor 6 | { 7 | string ExtractIndex( 8 | string memberName, 9 | IEntityConfiguration entityConfiguration, 10 | Table tableInfo = null) 11 | where TEntity : class, new(); 12 | 13 | string TryExtractIndex( 14 | string memberName, 15 | IEntityConfiguration entityConfiguration, 16 | Table tableInfo = null) 17 | where TEntity : class, new(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace EasyDynamo.Extensions 6 | { 7 | public static class CollectionExtensions 8 | { 9 | public static string JoinByNewLine(this IEnumerable source) 10 | { 11 | return string.Join(Environment.NewLine, source); 12 | } 13 | 14 | public static string JoinByNewLine( 15 | this IEnumerable> source) 16 | { 17 | return JoinByNewLine(source.Select(kvp => $"{kvp.Key}:{kvp.Value}")); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/EntityTypeConfugurationFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using System; 3 | 4 | namespace EasyDynamo.Tests.Fakes 5 | { 6 | public class EntityTypeConfugurationFake : IEntityTypeConfiguration 7 | { 8 | public bool ConfigureInvoked { get; private set; } 9 | 10 | public Type ConfigureInvokedWithBuilderType { get; private set; } 11 | 12 | public void Configure(IEntityTypeBuilder builder) 13 | { 14 | this.ConfigureInvoked = true; 15 | this.ConfigureInvokedWithBuilderType = builder.GetType(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IPrimaryKeyExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DocumentModel; 2 | 3 | namespace EasyDynamo.Abstractions 4 | { 5 | public interface IPrimaryKeyExtractor 6 | { 7 | object ExtractPrimaryKey( 8 | TEntity entity, 9 | IEntityConfiguration entityConfiguration, 10 | Table tableInfo = null) 11 | where TEntity : class, new(); 12 | 13 | object TryExtractPrimaryKey( 14 | TEntity entity, 15 | IEntityConfiguration entityConfiguration, 16 | Table tableInfo = null) 17 | where TEntity : class, new(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/DynamoDBContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using Amazon.DynamoDBv2.DocumentModel; 3 | 4 | namespace EasyDynamo.Extensions 5 | { 6 | public static class DynamoDBContextExtensions 7 | { 8 | public static Table TryGetTargetTable( 9 | this IDynamoDBContext dynamoDBContext, 10 | DynamoDBOperationConfig operationConfig = null) 11 | { 12 | try 13 | { 14 | return dynamoDBContext.GetTargetTable(operationConfig); 15 | } 16 | catch 17 | { 18 | return default(Table); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace EasyDynamo.Abstractions 6 | { 7 | public interface IEntityConfiguration : IEntityConfigurationMetadata 8 | { 9 | string HashKeyMemberName { get; set; } 10 | 11 | Type HashKeyMemberType { get; set; } 12 | 13 | ISet IgnoredMembersNames { get; } 14 | 15 | ISet Indexes { get; } 16 | 17 | string TableName { get; set; } 18 | 19 | long ReadCapacityUnits { get; set; } 20 | 21 | long WriteCapacityUnits { get; set; } 22 | 23 | bool ValidateOnSave { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/TestRetrier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EasyDynamo.Tests 5 | { 6 | public static class TestRetrier 7 | { 8 | public static async Task RetryAsync(Action action, int retries = 5, int delay = 100) 9 | { 10 | var passed = false; 11 | 12 | while (!passed && retries > 0) 13 | { 14 | try 15 | { 16 | action(); 17 | 18 | passed = true; 19 | } 20 | catch 21 | { 22 | retries--; 23 | 24 | await Task.Delay(delay); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/RequestFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class RequestFailedException : Exception 7 | { 8 | public RequestFailedException() 9 | { 10 | } 11 | 12 | public RequestFailedException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public RequestFailedException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | protected RequestFailedException(SerializationInfo info, StreamingContext context) 23 | : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using EasyDynamo.Core; 3 | 4 | namespace EasyDynamo.Abstractions 5 | { 6 | public interface IEntityConfigurationProvider 7 | { 8 | IEntityConfiguration GetEntityConfiguration() 9 | where TContext : DynamoContext 10 | where TEntity : class; 11 | 12 | IEntityConfiguration TryGetEntityConfiguration() 13 | where TContext : DynamoContext 14 | where TEntity : class; 15 | 16 | IEntityConfiguration GetEntityConfiguration(Type contextType, Type entityType); 17 | 18 | IEntityConfiguration TryGetEntityConfiguration(Type contextType, Type entityType); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class EntityNotFoundException : Exception 7 | { 8 | public EntityNotFoundException() 9 | { 10 | } 11 | 12 | public EntityNotFoundException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public EntityNotFoundException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | protected EntityNotFoundException(SerializationInfo info, StreamingContext context) 23 | : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo/Config/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace EasyDynamo.Config 5 | { 6 | public static class Constants 7 | { 8 | public const long DefaultReadCapacityUnits = 1; 9 | public const long DefaultWriteCapacityUnits = 1; 10 | 11 | public static readonly IDictionary AttributeTypesMap = 12 | new Dictionary 13 | { 14 | [typeof(string)] = "S", 15 | [typeof(int)] = "N", 16 | [typeof(byte)] = "B", 17 | [typeof(DateTime)] = "S", 18 | [typeof(Enum)] = "N", 19 | [typeof(byte[])] = "BS", 20 | [typeof(string[])] = "SS", 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/CreateTableFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class CreateTableFailedException : RequestFailedException 7 | { 8 | public CreateTableFailedException() 9 | { 10 | } 11 | 12 | public CreateTableFailedException(string message) : base(message) 13 | { 14 | } 15 | 16 | public CreateTableFailedException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | protected CreateTableFailedException(SerializationInfo info, StreamingContext context) 22 | : base(info, context) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/DeleteTableFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class DeleteTableFailedException : RequestFailedException 7 | { 8 | public DeleteTableFailedException() 9 | { 10 | } 11 | 12 | public DeleteTableFailedException(string message) : base(message) 13 | { 14 | } 15 | 16 | public DeleteTableFailedException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | protected DeleteTableFailedException(SerializationInfo info, StreamingContext context) 22 | : base(info, context) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/UpdateTableFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class UpdateTableFailedException : RequestFailedException 7 | { 8 | public UpdateTableFailedException() 9 | { 10 | } 11 | 12 | public UpdateTableFailedException(string message) : base(message) 13 | { 14 | } 15 | 16 | public UpdateTableFailedException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | protected UpdateTableFailedException(SerializationInfo info, StreamingContext context) 22 | : base(info, context) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/EntityAlreadyExistException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class EntityAlreadyExistException : Exception 7 | { 8 | public EntityAlreadyExistException() 9 | { 10 | } 11 | 12 | public EntityAlreadyExistException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public EntityAlreadyExistException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | protected EntityAlreadyExistException(SerializationInfo info, StreamingContext context) 23 | : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/EntityValidationFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class EntityValidationFailedException : Exception 7 | { 8 | public EntityValidationFailedException() 9 | { 10 | } 11 | 12 | public EntityValidationFailedException(string message) : base(message) 13 | { 14 | } 15 | 16 | public EntityValidationFailedException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | protected EntityValidationFailedException(SerializationInfo info, StreamingContext context) 22 | : base(info, context) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/DynamoDbIndexMissingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class DynamoDbIndexMissingException : Exception 7 | { 8 | public DynamoDbIndexMissingException() 9 | { 10 | } 11 | 12 | public DynamoDbIndexMissingException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public DynamoDbIndexMissingException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | protected DynamoDbIndexMissingException(SerializationInfo info, StreamingContext context) 23 | : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/FakeEntity.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace EasyDynamo.Tests.Fakes 6 | { 7 | public class FakeEntity 8 | { 9 | public const string IndexName = "GSI_FakeEntity_Title_LastModified"; 10 | 11 | public int Id { get; set; } 12 | 13 | [DynamoDBGlobalSecondaryIndexHashKey(IndexName)] 14 | public string Title { get; set; } 15 | 16 | public string Content { get; set; } 17 | 18 | [DynamoDBGlobalSecondaryIndexRangeKey(IndexName)] 19 | public DateTime LastModified { get; set; } 20 | 21 | public string IgnoreMe { get; set; } 22 | 23 | [Required] 24 | public string PropertyWithRequiredAttribute { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/DynamoContextOptionsFake.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace EasyDynamo.Tests.Fakes 6 | { 7 | public class DynamoContextOptionsFake : DynamoContextOptions 8 | { 9 | protected internal DynamoContextOptionsFake(Type contextType) 10 | : base(contextType) 11 | { 12 | } 13 | 14 | public IDictionary TableNameByEntityTypesFromBase 15 | => base.TableNameByEntityTypes; 16 | 17 | protected internal void ValidateCloudModeFromBase() 18 | { 19 | base.ValidateCloudMode(); 20 | } 21 | 22 | protected internal void ValidateLocalModeFromBase() 23 | { 24 | base.ValidateLocalMode(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo/Exceptions/DynamoContextConfigurationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace EasyDynamo.Exceptions 5 | { 6 | public class DynamoContextConfigurationException : Exception 7 | { 8 | public DynamoContextConfigurationException() 9 | { 10 | } 11 | 12 | public DynamoContextConfigurationException(string message) 13 | : base(message) 14 | { 15 | } 16 | 17 | public DynamoContextConfigurationException(string message, Exception innerException) 18 | : base(message, innerException) 19 | { 20 | } 21 | 22 | protected DynamoContextConfigurationException(SerializationInfo info, StreamingContext context) 23 | : base(info, context) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/EasyDynamo.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/EasyDynamo/Config/ExceptionMessage.cs: -------------------------------------------------------------------------------- 1 | namespace EasyDynamo.Config 2 | { 3 | public class ExceptionMessage 4 | { 5 | public const string EntityAlreadyExist = "The entity already exist."; 6 | public const string EntityDoesNotExist = "The entity does not exist."; 7 | public const string HashKeyConfigurationNotFound = 8 | "HashKey configuration not found. " + 9 | "Specify a hash key using DynamoDBHashKey attribute or override " + 10 | "the OnModelCreating of your DynamoContext."; 11 | public const string EntityIndexNotFound = "Could not find an index for member: {0}.{1}."; 12 | public const string EntityConfigurationNotFound = 13 | "Configuration for {0} not found. " + 14 | "Use DynamoDBAttributes in your entity class " + 15 | "or override the OnModelCreating method in your database context class."; 16 | public const string PositiveIntegerNeeded ="{0} should be a positive integer."; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IDynamoContextOptions.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.DynamoDBv2; 3 | using Amazon.Extensions.NETCore.Setup; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace EasyDynamo.Abstractions 8 | { 9 | public interface IDynamoContextOptions 10 | { 11 | Type ContextType { get; } 12 | 13 | string AccessKeyId { get; set; } 14 | 15 | bool LocalMode { get; set; } 16 | 17 | string Profile { get; set; } 18 | 19 | RegionEndpoint RegionEndpoint { get; set; } 20 | 21 | string SecretAccessKey { get; set; } 22 | 23 | string ServiceUrl { get; set; } 24 | 25 | DynamoDBEntryConversion Conversion { get; set; } 26 | 27 | AWSOptions AwsOptions { get; set; } 28 | 29 | IDictionary TableNameByEntityTypes { get; } 30 | 31 | void UseTableName(string tableName) where TEntity : class, new(); 32 | 33 | void ValidateCloudMode(); 34 | 35 | void ValidateLocalMode(); 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mihail Sotirov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Fakes/FakeDynamoContext.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Builders; 3 | using EasyDynamo.Core; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace EasyDynamo.Tests.Fakes 7 | { 8 | public class FakeDynamoContext : DynamoContext 9 | { 10 | public FakeDynamoContext(IDependencyResolver dependencyResolver) 11 | : base(dependencyResolver) 12 | { 13 | } 14 | 15 | public static bool OnConfiguringInvoked { get; set; } 16 | 17 | public static bool OnModelCreatingInvoked { get; set; } 18 | 19 | protected override void OnConfiguring( 20 | DynamoContextOptionsBuilder builder, IConfiguration configuration) 21 | { 22 | OnConfiguringInvoked = true; 23 | 24 | base.OnConfiguring(builder, configuration); 25 | } 26 | 27 | protected override void OnModelCreating( 28 | ModelBuilder builder, IConfiguration configuration) 29 | { 30 | OnModelCreatingInvoked = true; 31 | 32 | base.OnModelCreating(builder, configuration); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace EasyDynamo.Extensions 6 | { 7 | public static class ExpressionExtensions 8 | { 9 | public static MemberExpression TryGetMemberExpression( 10 | this Expression> sourceExpression) 11 | { 12 | var memberExpr = sourceExpression.Body as MemberExpression 13 | ?? (sourceExpression.Body as UnaryExpression)?.Operand as MemberExpression; 14 | 15 | return memberExpr; 16 | } 17 | 18 | public static string TryGetMemberName( 19 | this Expression> sourceExpression) 20 | { 21 | return TryGetMemberExpression(sourceExpression)?.Member?.Name; 22 | } 23 | 24 | public static Type TryGetMemberType( 25 | this Expression> sourceExpression) 26 | { 27 | return (TryGetMemberExpression(sourceExpression)?.Member as PropertyInfo)?.PropertyType; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/EasyDynamo/Builders/PropertyTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Config; 3 | 4 | namespace EasyDynamo.Builders 5 | { 6 | public class PropertyTypeBuilder : IPropertyTypeBuilder 7 | { 8 | private readonly PropertyConfiguration configuration; 9 | 10 | protected internal PropertyTypeBuilder(PropertyConfiguration configuration) 11 | { 12 | this.configuration = configuration; 13 | } 14 | 15 | /// 16 | /// Adds a specific value as default. If a value is missing the specified default 17 | /// value will be set before saving the entity to the database. 18 | /// 19 | public IPropertyTypeBuilder HasDefaultValue(object defaultValue) 20 | { 21 | this.configuration.DefaultValue = defaultValue; 22 | 23 | return this; 24 | } 25 | 26 | /// 27 | /// Specifies either the member is required or not. 28 | /// 29 | public IPropertyTypeBuilder IsRequired(bool required = true) 30 | { 31 | this.configuration.IsRequired = required; 32 | 33 | return this; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/TableDropper.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Exceptions; 5 | using EasyDynamo.Extensions; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace EasyDynamo.Tools 10 | { 11 | public class TableDropper : ITableDropper 12 | { 13 | private readonly IAmazonDynamoDB client; 14 | 15 | public TableDropper(IAmazonDynamoDB client) 16 | { 17 | this.client = client; 18 | } 19 | 20 | public async Task DropTableAsync(string tableName) 21 | { 22 | var request = new DeleteTableRequest(tableName); 23 | 24 | try 25 | { 26 | var response = await this.client.DeleteTableAsync(request); 27 | 28 | if (!response.HttpStatusCode.IsSuccessful()) 29 | { 30 | throw new DeleteTableFailedException( 31 | response.ResponseMetadata?.Metadata?.JoinByNewLine()); 32 | } 33 | } 34 | catch (Exception ex) 35 | { 36 | throw new DeleteTableFailedException("Failed to delete a table.", ex); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Resolvers/ServiceProviderDependencyResolver.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using System; 3 | 4 | namespace EasyDynamo.Tools.Resolvers 5 | { 6 | public class ServiceProviderDependencyResolver : IDependencyResolver 7 | { 8 | private readonly IServiceProvider serviceProvider; 9 | 10 | public ServiceProviderDependencyResolver(IServiceProvider serviceProvider) 11 | { 12 | this.serviceProvider = serviceProvider; 13 | } 14 | 15 | public TDependency GetDependency() 16 | { 17 | return (TDependency)this.GetDependency(typeof(TDependency)); 18 | } 19 | 20 | public object GetDependency(Type dependencyType) 21 | { 22 | return this.serviceProvider.GetService(dependencyType) 23 | ?? throw new InvalidOperationException( 24 | $"Unable to resolve service of type {dependencyType.FullName}."); 25 | } 26 | 27 | public object TryGetDependency(Type dependencyType) 28 | { 29 | try 30 | { 31 | return this.GetDependency(dependencyType); 32 | } 33 | catch 34 | { 35 | return default; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /doc/main.md: -------------------------------------------------------------------------------- 1 | ## EasyDynamo - fluent configuring and access for DynamoDB with code first aproach. 2 | 3 | ------------ 4 | 5 | EasyDynamo is a small library built using .Net Core 2.1 that helps developers to access and configure DynamoDB easier. Different configurations can be applied for different environments (development, staging, production) as well as using a local dynamo instance for non production environment. Supports creating dynamo tables correspondings to your models using code first aproach. 6 | 7 | ### Installation: 8 | You can install this library using NuGet into your project. 9 | 10 | `Install-Package EasyDynamo` 11 | 12 | ### See how to: 13 | - ### [configure the database access](https://github.com/msotiroff/EasyDynamo/blob/master/doc/configure-access.md "configure the database access") 14 | 15 | - ### [configure your models](https://github.com/msotiroff/EasyDynamo/blob/master/doc/configure-models.md "configure your models") 16 | 17 | - ### [configure a local instance of DynamoDB](https://github.com/msotiroff/EasyDynamo/blob/master/doc/configure-local-client.md "configure a local instance of DynamoDB") 18 | 19 | - ### [create a dynamo table by code first aproach](https://github.com/msotiroff/EasyDynamo/blob/master/doc/code-first.md "create a dynamo table by code first aproach") 20 | 21 | - ### [work with your database context class](https://github.com/msotiroff/EasyDynamo/blob/master/doc/dynamo-context.md "work with your database context class") -------------------------------------------------------------------------------- /src/EasyDynamo/Config/GlobalSecondaryIndexConfiguration.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Tools.Validators; 2 | using System; 3 | 4 | namespace EasyDynamo.Config 5 | { 6 | public class GlobalSecondaryIndexConfiguration 7 | { 8 | private long readCapacityUnits; 9 | private long writeCapacityUnits; 10 | 11 | public GlobalSecondaryIndexConfiguration() 12 | { 13 | this.ReadCapacityUnits = 1; 14 | this.WriteCapacityUnits = 1; 15 | } 16 | 17 | public string IndexName { get; set; } 18 | 19 | public string HashKeyMemberName { get; set; } 20 | 21 | public Type HashKeyMemberType { get; set; } 22 | 23 | public string RangeKeyMemberName { get; set; } 24 | 25 | public Type RangeKeyMemberType { get; set; } 26 | 27 | public long ReadCapacityUnits 28 | { 29 | get => this.readCapacityUnits; 30 | set 31 | { 32 | InputValidator.ThrowIfNotPositive(value, string.Format( 33 | ExceptionMessage.PositiveIntegerNeeded, nameof(this.ReadCapacityUnits))); 34 | 35 | this.readCapacityUnits = value; 36 | } 37 | } 38 | 39 | public long WriteCapacityUnits 40 | { 41 | get => this.writeCapacityUnits; 42 | set 43 | { 44 | InputValidator.ThrowIfNotPositive(value, string.Format( 45 | ExceptionMessage.PositiveIntegerNeeded, nameof(this.WriteCapacityUnits))); 46 | 47 | this.writeCapacityUnits = value; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | using System.Runtime.Serialization.Formatters.Binary; 4 | 5 | namespace EasyDynamo.Extensions 6 | { 7 | public static class ObjectExtensions 8 | { 9 | public static T Clone(this T source) 10 | { 11 | var destination = TryClone(source); 12 | 13 | return destination; 14 | } 15 | 16 | private static T TryClone(T source) 17 | { 18 | try 19 | { 20 | using (var ms = new MemoryStream()) 21 | { 22 | var formatter = new BinaryFormatter(); 23 | formatter.Serialize(ms, source); 24 | ms.Position = 0; 25 | 26 | return (T)formatter.Deserialize(ms); 27 | } 28 | } 29 | catch 30 | { 31 | return TryCloneUsingJsonSerialization(source); 32 | } 33 | } 34 | 35 | private static T TryCloneUsingJsonSerialization(T source) 36 | { 37 | try 38 | { 39 | var settings = new JsonSerializerSettings 40 | { 41 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore 42 | }; 43 | var serialized = JsonConvert.SerializeObject(source, settings); 44 | var deserialized = JsonConvert.DeserializeObject(serialized); 45 | 46 | return deserialized; 47 | } 48 | catch 49 | { 50 | return default(T); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EasyDynamo.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2019 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EasyDynamo", "EasyDynamo\EasyDynamo.csproj", "{57EE69A8-FC3D-4778-9F99-360C4487CF37}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EasyDynamo.Tests", "EasyDynamo.Tests\EasyDynamo.Tests.csproj", "{0830C2DE-137E-47B4-8545-291616E4C613}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {57EE69A8-FC3D-4778-9F99-360C4487CF37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {57EE69A8-FC3D-4778-9F99-360C4487CF37}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {57EE69A8-FC3D-4778-9F99-360C4487CF37}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {57EE69A8-FC3D-4778-9F99-360C4487CF37}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {0830C2DE-137E-47B4-8545-291616E4C613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {0830C2DE-137E-47B4-8545-291616E4C613}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {0830C2DE-137E-47B4-8545-291616E4C613}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {0830C2DE-137E-47B4-8545-291616E4C613}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {CEC387A6-B865-4212-814C-9E8F1836DA75} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IEntityTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Config; 2 | using EasyDynamo.Core; 3 | using System; 4 | using System.Linq.Expressions; 5 | 6 | namespace EasyDynamo.Abstractions 7 | { 8 | public interface IEntityTypeBuilder 9 | where TContext : DynamoContext 10 | where TEntity : class 11 | { 12 | IEntityTypeBuilder HasTable(string tableName); 13 | 14 | IEntityTypeBuilder HasGlobalSecondaryIndex( 15 | string indexName, 16 | Expression> hashKeyMemberExpression, 17 | Expression> rangeKeyMemberExpression, 18 | long readCapacityUnits = 1, 19 | long writeCapacityUnits = 1); 20 | 21 | IEntityTypeBuilder HasGlobalSecondaryIndex( 22 | Action indexAction); 23 | 24 | IEntityTypeBuilder HasPrimaryKey( 25 | Expression> keyExpression); 26 | 27 | IEntityTypeBuilder HasReadCapacityUnits(long readCapacityUnits); 28 | 29 | IEntityTypeBuilder HasWriteCapacityUnits(long writeCapacityUnits); 30 | 31 | IEntityTypeBuilder Ignore( 32 | Expression> propertyExpression); 33 | 34 | IEntityTypeBuilder Ignore(string propertyName); 35 | 36 | IEntityTypeBuilder ValidateOnSave(bool validate = true); 37 | 38 | IPropertyTypeBuilder Property( 39 | Expression> propertyExpression); 40 | } 41 | } -------------------------------------------------------------------------------- /doc/configure-local-client.md: -------------------------------------------------------------------------------- 1 | ### How to configure a local instance of DynamoDB 2 | You can run dynamo locally using [Docker Image](https://hub.docker.com/r/amazon/dynamodb-local/ "Docker Image") or [DynamoDBLocal.jar file](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html "DynamoDBLocal.jar file"). 3 | ##### How to configure local dynamo for a specific environment? 4 | Important: if a local mode is enabled you should specify the ServiceUrl with the port you use for the local instance of DynamoDB! 5 | ##### 1. In Startup.cs: 6 | ```csharp 7 | services.AddDynamoContext(this.Configuration, options => 8 | { 9 | options.LocalMode = this.Configuration 10 | .GetValue("DynamoOptions:LocalMode"); 11 | options.ServiceUrl = this.Configuration 12 | .GetValue("DynamoOptions:ServiceUrl"); 13 | }); 14 | ``` 15 | appsettings.json: 16 | ```json 17 | { 18 | "DynamoOptions": { 19 | "LocalMode": false 20 | } 21 | ``` 22 | appsettings.Development.json: 23 | ```json 24 | { 25 | "DynamoOptions": { 26 | "LocalMode": true, 27 | "ServiceUrl": "http://localhost:8000" 28 | } 29 | ``` 30 | This code will run local client when environment is Development and cloud mode when it's production. 31 | 32 | #### 2. In your database context class: 33 | ```csharp 34 | protected override void OnConfiguring( 35 | DynamoContextOptionsBuilder builder, IConfiguration configuration) 36 | { 37 | var shouldUseLocalInstance = configuration.GetValue("DynamoOptions:LocalMode"); 38 | 39 | if (shouldUseLocalInstance) 40 | { 41 | var serviceUrl = configuration.GetValue("DynamoOptions:ServiceUrl"); 42 | 43 | builder.UseLocalMode(serviceUrl); 44 | } 45 | 46 | base.OnConfiguring(builder, configuration); 47 | } 48 | ``` -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Validators/InputValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EasyDynamo.Tools.Validators 4 | { 5 | public static class InputValidator 6 | { 7 | public static void ThrowIfNotPositive(long value, string errorMessage = null) 8 | { 9 | if (value <= 0) 10 | { 11 | throw new ArgumentException( 12 | errorMessage ?? "Value should be a positive integer."); 13 | } 14 | } 15 | 16 | public static void ThrowIfNullOrWhitespace(string value, string errorMessage = null) 17 | { 18 | if (!string.IsNullOrWhiteSpace(value)) 19 | { 20 | return; 21 | } 22 | 23 | throw new ArgumentNullException(errorMessage ?? "Value cannot be empty."); 24 | } 25 | 26 | public static void ThrowIfAnyNullOrWhitespace(params object[] values) 27 | { 28 | foreach (var value in values) 29 | { 30 | if (value is string) 31 | { 32 | ThrowIfNullOrWhitespace(value as string); 33 | 34 | continue; 35 | } 36 | 37 | ThrowIfNull(value); 38 | } 39 | } 40 | 41 | public static void ThrowIfNull(T item, string errorMessage = null) where T : class 42 | { 43 | if (item != null) 44 | { 45 | return; 46 | } 47 | 48 | var fallbackErrorMessage = "Value cannot be null."; 49 | 50 | throw new ArgumentNullException(errorMessage ?? fallbackErrorMessage); 51 | } 52 | 53 | public static void ThrowIfAnyNull(params object[] items) 54 | { 55 | foreach (var item in items) 56 | { 57 | ThrowIfNull(item); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Providers/DynamoContextOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Core; 3 | using EasyDynamo.Exceptions; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace EasyDynamo.Tools.Providers 9 | { 10 | public class DynamoContextOptionsProvider : IDynamoContextOptionsProvider 11 | { 12 | private readonly IEnumerable allOptions; 13 | 14 | public DynamoContextOptionsProvider(IEnumerable options = null) 15 | { 16 | this.allOptions = options ?? new List(); 17 | } 18 | 19 | public IDynamoContextOptions GetContextOptions() 20 | where TContext : DynamoContext 21 | { 22 | return this.GetContextOptions(typeof(TContext)); 23 | } 24 | 25 | public IDynamoContextOptions GetContextOptions(Type contextType) 26 | { 27 | if (!typeof(DynamoContext).IsAssignableFrom(contextType)) 28 | { 29 | throw new DynamoContextConfigurationException( 30 | $"{contextType.Name} does not inherit from {nameof(DynamoContext)}."); 31 | } 32 | 33 | var options = this.allOptions.FirstOrDefault(o => o.ContextType == contextType); 34 | 35 | return options ?? 36 | throw new DynamoContextConfigurationException( 37 | $"Could not find any options for {contextType.Name}."); 38 | } 39 | 40 | public IDynamoContextOptions TryGetContextOptions() 41 | where TContext : DynamoContext 42 | { 43 | try 44 | { 45 | return this.GetContextOptions(); 46 | } 47 | catch 48 | { 49 | return default; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/EasyDynamo/EasyDynamo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | EasyDynamo is a small library that helps developers to access and configure DynamoDB easier. Different configurations can be applied for different environments (development, staging, production) as well as using a local dynamo instance for non production environment. Supports creating dynamo tables correspondings to your models using code first aproach. 6 | https://github.com/msotiroff/EasyDynamo/tree/master/src 7 | https://github.com/msotiroff/EasyDynamo 8 | https://github.com/msotiroff/EasyDynamo/blob/master/logo.jpg?raw=true 9 | 10 | Public 11 | AWS, Amazon, DynamoDB 12 | true 13 | 1.1.6 14 | Mihail Sotirov 15 | 16 | 1.1.6.0 17 | 1.1.6.0 18 | LICENSE.txt 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/EasyDynamo/Abstractions/IDynamoDbSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Amazon.DynamoDBv2; 6 | using Amazon.DynamoDBv2.DataModel; 7 | using Amazon.DynamoDBv2.DocumentModel; 8 | using EasyDynamo.Attributes; 9 | using EasyDynamo.Core; 10 | 11 | namespace EasyDynamo.Abstractions 12 | { 13 | [DynamoDbSet] 14 | public interface IDynamoDbSet where TEntity : class, new() 15 | { 16 | IDynamoDBContext Base { get; } 17 | 18 | IAmazonDynamoDB Client { get; } 19 | 20 | string TableName { get; } 21 | 22 | Task AddAsync(TEntity entity); 23 | 24 | Task> FilterAsync( 25 | Expression> conditionExpression); 26 | 27 | Task> FilterAsync( 28 | string memberName, object value, string indexName = null); 29 | 30 | Task> FilterAsync( 31 | Expression> propertyExpression, 32 | ScanOperator scanOperator, 33 | TProperty value); 34 | 35 | Task> FilterAsync( 36 | Expression> propertyExpression, 37 | TProperty value, 38 | string indexName = null); 39 | 40 | Task> GetAsync(); 41 | 42 | Task> GetAsync( 43 | int itemsPerPage, string paginationToken, string indexName = null); 44 | 45 | Task GetAsync(object primaryKey); 46 | 47 | Task GetAsync(object primaryKey, object rangeKey); 48 | 49 | Task RemoveAsync(object primaryKey); 50 | 51 | Task RemoveAsync(TEntity entity); 52 | 53 | Task SaveAsync(TEntity entity); 54 | 55 | Task SaveManyAsync(IEnumerable entities); 56 | 57 | Task UpdateAsync(TEntity entity); 58 | } 59 | } -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Extractors/PrimaryKeyExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using Amazon.DynamoDBv2.DocumentModel; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Config; 5 | using EasyDynamo.Exceptions; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace EasyDynamo.Tools.Extractors 10 | { 11 | public class PrimaryKeyExtractor : IPrimaryKeyExtractor 12 | { 13 | public object ExtractPrimaryKey( 14 | TEntity entity, 15 | IEntityConfiguration entityConfiguration, 16 | Table tableInfo = null) 17 | where TEntity : class, new() 18 | { 19 | var keyExpression = entityConfiguration.HashKeyMemberExpression; 20 | 21 | if (keyExpression != null) 22 | { 23 | var keyExtractFunction = keyExpression.Compile(); 24 | 25 | return keyExtractFunction(entity); 26 | } 27 | 28 | var keyMember = typeof(TEntity) 29 | .GetProperties() 30 | .FirstOrDefault(pi => pi.GetCustomAttribute() != null) 31 | ?? typeof(TEntity) 32 | .GetProperty(tableInfo?.HashKeys?.FirstOrDefault() ?? string.Empty); 33 | 34 | if (keyMember != null) 35 | { 36 | return keyMember.GetValue(entity); 37 | } 38 | 39 | throw new DynamoContextConfigurationException( 40 | ExceptionMessage.HashKeyConfigurationNotFound); 41 | } 42 | 43 | public object TryExtractPrimaryKey( 44 | TEntity entity, 45 | IEntityConfiguration entityConfiguration, 46 | Table tableInfo = null) 47 | where TEntity : class, new() 48 | { 49 | try 50 | { 51 | return this.ExtractPrimaryKey(entity, entityConfiguration, tableInfo); 52 | } 53 | catch 54 | { 55 | return default(object); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Builders/PropertyTypeBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Tests.Fakes; 2 | using Xunit; 3 | 4 | namespace EasyDynamo.Tests.Builders 5 | { 6 | public class PropertyTypeBuilderTests 7 | { 8 | private const string SampleDefaultValue = "Some content"; 9 | private readonly string propertyName; 10 | private readonly PropertyConfigurationFake propertyConfig; 11 | private readonly PropertyTypeBuilderFake builder; 12 | 13 | public PropertyTypeBuilderTests() 14 | { 15 | this.propertyName = nameof(FakeEntity.Content); 16 | this.propertyConfig = new PropertyConfigurationFake(this.propertyName); 17 | this.builder = new PropertyTypeBuilderFake(this.propertyConfig); 18 | } 19 | 20 | [Fact] 21 | public void HasDefaultValue_SetCorrectValue() 22 | { 23 | this.builder.HasDefaultValue(SampleDefaultValue); 24 | 25 | Assert.Equal(SampleDefaultValue, this.propertyConfig.DefaultValue); 26 | } 27 | 28 | [Fact] 29 | public void HasDefaultValue_ReturnsSameInstanceOfBuilder() 30 | { 31 | var returnedBuilder = this.builder.HasDefaultValue(SampleDefaultValue); 32 | 33 | Assert.Equal(this.builder, returnedBuilder); 34 | } 35 | 36 | [Theory] 37 | [InlineData(true)] 38 | [InlineData(false)] 39 | public void IsRequired_SetCorrectValue(bool value) 40 | { 41 | this.builder.IsRequired(value); 42 | 43 | Assert.Equal(value, this.propertyConfig.IsRequired); 44 | } 45 | 46 | [Fact] 47 | public void IsRequired_DefaultToTrue() 48 | { 49 | this.builder.IsRequired(); 50 | 51 | Assert.True(this.propertyConfig.IsRequired); 52 | } 53 | 54 | [Fact] 55 | public void IsRequired_ReturnsSameInstanceOfBuilder() 56 | { 57 | var returnedBuilder = this.builder.IsRequired(true); 58 | 59 | Assert.Equal(this.builder, returnedBuilder); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/EasyDynamo/Factories/IndexFactory.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Config; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace EasyDynamo.Factories 9 | { 10 | public class IndexFactory : IIndexFactory 11 | { 12 | public IEnumerable CreateRequestIndexes( 13 | IEnumerable indexes) 14 | { 15 | indexes = indexes ?? Enumerable.Empty(); 16 | var gsis = new List(); 17 | 18 | foreach (var index in indexes) 19 | { 20 | if (string.IsNullOrWhiteSpace(index.IndexName) || 21 | string.IsNullOrWhiteSpace(index.HashKeyMemberName)) 22 | { 23 | continue; 24 | } 25 | 26 | var gsi = new GlobalSecondaryIndex 27 | { 28 | IndexName = index.IndexName, 29 | Projection = new Projection 30 | { 31 | ProjectionType = ProjectionType.ALL 32 | }, 33 | ProvisionedThroughput = new ProvisionedThroughput 34 | { 35 | ReadCapacityUnits = index.ReadCapacityUnits, 36 | WriteCapacityUnits = index.WriteCapacityUnits 37 | }, 38 | KeySchema = new List 39 | { 40 | new KeySchemaElement(index.HashKeyMemberName, KeyType.HASH) 41 | } 42 | }; 43 | 44 | if (!string.IsNullOrWhiteSpace(index.RangeKeyMemberName)) 45 | { 46 | gsi.KeySchema.Add( 47 | new KeySchemaElement(index.RangeKeyMemberName, KeyType.RANGE)); 48 | } 49 | 50 | gsis.Add(gsi); 51 | } 52 | 53 | return gsis; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /doc/code-first.md: -------------------------------------------------------------------------------- 1 | ### How to create a dynamo table by code first aproach 2 | 3 | You can run a simple code (using the facade method EnsureCreatedAsync) on building your application to ensure all the tables for your models are created: 4 | 5 | #### 1. Directly get the context instance from the application services in Startup.cs (not recommended, because the resourses can be disposed before tables have been created). 6 | ```csharp 7 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 8 | { 9 | if (env.IsDevelopment()) 10 | { 11 | app.UseDeveloperExceptionPage(); 12 | } 13 | else 14 | { 15 | app.UseExceptionHandler("/Error"); 16 | app.UseHsts(); 17 | } 18 | 19 | app.UseMvc(); 20 | 21 | var context = app.ApplicationServices.GetRequiredService(); 22 | 23 | context.Database.EnsureCreatedAsync().Wait(); 24 | } 25 | ``` 26 | #### 2. The right way => extension over IApplicationBuilder and using resourses in a separate scope. 27 | Add an extention method in a separate class: 28 | ```csharp 29 | public static class ApplicationBuilderExtensions 30 | { 31 | public static IApplicationBuilder EnsureDatabaseCreated(this IApplicationBuilder app) 32 | { 33 | var scopeFactory = app.ApplicationServices.GetRequiredService(); 34 | 35 | using (var scope = scopeFactory.CreateScope()) 36 | { 37 | var context = scope.ServiceProvider.GetRequiredService(); 38 | 39 | Task.Run(async () => 40 | { 41 | await context.Database.EnsureCreatedAsync(); 42 | }) 43 | .Wait(); 44 | } 45 | 46 | return app; 47 | } 48 | } 49 | ``` 50 | Then call this method in Startup.cs: 51 | ```csharp 52 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 53 | { 54 | if (env.IsDevelopment()) 55 | { 56 | app.UseDeveloperExceptionPage(); 57 | } 58 | else 59 | { 60 | app.UseExceptionHandler("/Error"); 61 | app.UseHsts(); 62 | } 63 | 64 | app.UseMvc(); 65 | 66 | app.EnsureDatabaseCreated(); 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Extractors/TableNameExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.DocumentModel; 3 | using Amazon.DynamoDBv2.Model; 4 | using EasyDynamo.Abstractions; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace EasyDynamo.Tools.Extractors 9 | { 10 | public class TableNameExtractor : ITableNameExtractor 11 | { 12 | private readonly IAmazonDynamoDB client; 13 | 14 | public TableNameExtractor(IAmazonDynamoDB client) 15 | { 16 | this.client = client; 17 | } 18 | 19 | public string ExtractTableName( 20 | IDynamoContextOptions options, 21 | IEntityConfiguration entityConfiguration, 22 | Table tableInfo = null) 23 | where TEntity : class, new() 24 | { 25 | var fromTableInfo = tableInfo?.TableName; 26 | var fromEntityConfig = entityConfiguration.TableName; 27 | var tableNamesByEntityTypes = options.TableNameByEntityTypes; 28 | var fromContextConfig = tableNamesByEntityTypes.ContainsKey(typeof(TEntity)) 29 | ? tableNamesByEntityTypes[typeof(TEntity)] 30 | : default(string); 31 | 32 | return fromEntityConfig 33 | ?? fromContextConfig 34 | ?? fromTableInfo 35 | ?? typeof(TEntity).Name; 36 | } 37 | 38 | public async Task> ExtractAllTableNamesAsync() 39 | { 40 | var allTableNames = new List(); 41 | var lastEvaluatedTableName = default(string); 42 | 43 | do 44 | { 45 | var request = new ListTablesRequest 46 | { 47 | ExclusiveStartTableName = lastEvaluatedTableName 48 | }; 49 | 50 | var response = await this.client.ListTablesAsync(request); 51 | 52 | allTableNames.AddRange(response.TableNames); 53 | 54 | lastEvaluatedTableName = response.LastEvaluatedTableName; 55 | } 56 | while (lastEvaluatedTableName != null); 57 | 58 | return allTableNames; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /doc/configure-models.md: -------------------------------------------------------------------------------- 1 | ### How to configure your database models 2 | 3 | #### 1. You can use standart DynamoDB attributes in your model class like that: 4 | 5 | ```csharp 6 | [DynamoDBTable("blog_articles_production")] 7 | public class Article 8 | { 9 | [DynamoDBHashKey] 10 | public string Id { get; set; } 11 | 12 | [DynamoDBGlobalSecondaryIndexHashKey("gsi_articles_title")] 13 | public string Title { get; set; } 14 | 15 | public string Content { get; set; } 16 | 17 | [DynamoDBGlobalSecondaryIndexRangeKey("gsi_articles_title")] 18 | public DateTime CreatedOn { get; set; } 19 | 20 | public string AuthorId { get; set; } 21 | } 22 | ``` 23 | ... but then you cannot use different tables for different environments! 24 | 25 | #### 2. Better way is you override OnModelCreating method in your database context class: 26 | 27 | ```csharp 28 | protected override void OnModelCreating(ModelBuilder builder, IConfiguration configuration) 29 | { 30 | builder.Entity
(entity => 31 | { 32 | entity.HasTable(configuration.GetValue("DynamoOptions:ArticlesTableName")); 33 | entity.HasPrimaryKey(a => a.Id); 34 | entity.HasGlobalSecondaryIndex(index => 35 | { 36 | index.IndexName = configuration 37 | .GetValue("DynamoOptions:Indexes:ArticleTitleGSI"); 38 | index.HashKeyMemberName = nameof(Article.Title); 39 | index.RangeKeyMemberName = nameof(Article.CreatedOn); 40 | index.ReadCapacityUnits = 3; 41 | index.WriteCapacityUnits = 3; 42 | }); 43 | entity.Property(a => a.CreatedOn).HasDefaultValue(DateTime.UtcNow); 44 | entity.Property(a => a.Content).IsRequired(); 45 | }); 46 | 47 | base.OnModelCreating(builder, configuration); 48 | } 49 | ``` 50 | appsettings.json: 51 | ```json 52 | { 53 | "DynamoOptions": { 54 | "ArticlesTableName": "blog_articles_production", 55 | "Indexes": { 56 | "ArticleTitleGSI": "gsi_articles_title" 57 | } 58 | } 59 | } 60 | ``` 61 | appsettings.Development.json: 62 | ```json 63 | { 64 | "DynamoOptions": { 65 | "ArticlesTableName": "blog_articles_development", 66 | "Indexes": { 67 | "ArticleTitleGSI": "gsi_articles_title" 68 | } 69 | } 70 | } 71 | ``` 72 | That way different table names will be applied for production and development environments. 73 | -------------------------------------------------------------------------------- /src/EasyDynamo/Factories/AttributeDefinitionFactory.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Config; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace EasyDynamo.Factories 9 | { 10 | public class AttributeDefinitionFactory : IAttributeDefinitionFactory 11 | { 12 | public IEnumerable CreateAttributeDefinitions( 13 | string primaryKeyMemberName, 14 | ScalarAttributeType primaryKeyMemberAttributeType, 15 | IEnumerable gsisConfiguration) 16 | { 17 | gsisConfiguration = gsisConfiguration 18 | ?? Enumerable.Empty(); 19 | 20 | var definitions = new List(); 21 | 22 | if (!string.IsNullOrWhiteSpace(primaryKeyMemberName) && 23 | primaryKeyMemberAttributeType != null) 24 | { 25 | definitions.Add(new AttributeDefinition 26 | { 27 | AttributeName = primaryKeyMemberName, 28 | AttributeType = primaryKeyMemberAttributeType 29 | }); 30 | } 31 | 32 | foreach (var gsiConfig in gsisConfiguration) 33 | { 34 | if (definitions.All(def => def.AttributeName != gsiConfig.HashKeyMemberName)) 35 | { 36 | definitions.Add(new AttributeDefinition 37 | { 38 | AttributeName = gsiConfig.HashKeyMemberName, 39 | AttributeType = Constants.AttributeTypesMap[gsiConfig.HashKeyMemberType] 40 | }); 41 | } 42 | 43 | if (!string.IsNullOrWhiteSpace(gsiConfig.RangeKeyMemberName) && 44 | definitions.All(def => def.AttributeName != gsiConfig.RangeKeyMemberName)) 45 | { 46 | definitions.Add(new AttributeDefinition 47 | { 48 | AttributeName = gsiConfig.RangeKeyMemberName, 49 | AttributeType = Constants.AttributeTypesMap[gsiConfig.RangeKeyMemberType] 50 | }); 51 | } 52 | } 53 | 54 | return definitions; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Extractors/IndexExtractor.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using Amazon.DynamoDBv2.DocumentModel; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Config; 5 | using EasyDynamo.Exceptions; 6 | using System.Linq; 7 | 8 | namespace EasyDynamo.Tools.Extractors 9 | { 10 | public class IndexExtractor : IIndexExtractor 11 | { 12 | public string ExtractIndex( 13 | string memberName, 14 | IEntityConfiguration entityConfiguration, 15 | Table tableInfo = null) 16 | where TEntity : class, new() 17 | { 18 | var fromModelConfig = entityConfiguration 19 | .Indexes 20 | ?.FirstOrDefault(i => i.HashKeyMemberName == memberName) 21 | ?.IndexName; 22 | var existInTableInfo = tableInfo 23 | ?.GlobalSecondaryIndexes 24 | ?.ContainsKey(memberName) ?? false; 25 | var fromTableInfo = existInTableInfo 26 | ? tableInfo.GlobalSecondaryIndexes[memberName].IndexName 27 | : null; 28 | var fromAttribute = ((DynamoDBGlobalSecondaryIndexHashKeyAttribute)typeof(TEntity) 29 | .GetProperty(memberName) 30 | ?.GetCustomAttributes(true) 31 | ?.SingleOrDefault(attr => 32 | typeof(DynamoDBGlobalSecondaryIndexHashKeyAttribute) 33 | .IsAssignableFrom(attr.GetType()))) 34 | ?.IndexNames 35 | ?.FirstOrDefault(); 36 | 37 | return fromModelConfig 38 | ?? fromAttribute 39 | ?? fromTableInfo 40 | ?? throw new DynamoDbIndexMissingException( 41 | string.Format(ExceptionMessage.EntityIndexNotFound, 42 | typeof(TEntity).FullName, 43 | memberName)); 44 | } 45 | 46 | public string TryExtractIndex( 47 | string memberName, 48 | IEntityConfiguration entityConfiguration, 49 | Table tableInfo = null) 50 | where TEntity : class, new() 51 | { 52 | try 53 | { 54 | return this.ExtractIndex(memberName, entityConfiguration, tableInfo); 55 | } 56 | catch (System.Exception) 57 | { 58 | return default; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Providers/EntityConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Core; 3 | using EasyDynamo.Exceptions; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace EasyDynamo.Tools.Providers 9 | { 10 | public class EntityConfigurationProvider : IEntityConfigurationProvider 11 | { 12 | private readonly IEnumerable allEntityConfigurations; 13 | 14 | public EntityConfigurationProvider( 15 | IEnumerable entityConfigurations) 16 | { 17 | this.allEntityConfigurations = entityConfigurations; 18 | } 19 | 20 | public IEntityConfiguration GetEntityConfiguration() 21 | where TContext : DynamoContext 22 | where TEntity : class 23 | { 24 | var configuration = this.TryGetEntityConfiguration(typeof(TContext), typeof(TEntity)); 25 | 26 | return configuration as IEntityConfiguration 27 | ?? throw new DynamoContextConfigurationException( 28 | $"Could not resolve service of type " + 29 | $"{typeof(IEntityConfiguration).FullName} " + 30 | $"with context {typeof(TContext).FullName}"); 31 | } 32 | 33 | public IEntityConfiguration GetEntityConfiguration(Type contextType, Type entityType) 34 | { 35 | var configuration = this.TryGetEntityConfiguration(contextType, entityType); 36 | 37 | return configuration 38 | ?? throw new DynamoContextConfigurationException( 39 | $"Could not resolve service of type " + 40 | $"{entityType.FullName} " + 41 | $"with context {contextType.FullName}"); 42 | } 43 | 44 | public IEntityConfiguration TryGetEntityConfiguration() 45 | where TContext : DynamoContext 46 | where TEntity : class 47 | { 48 | try 49 | { 50 | return this.GetEntityConfiguration(); 51 | } 52 | catch 53 | { 54 | return default; 55 | } 56 | } 57 | 58 | public IEntityConfiguration TryGetEntityConfiguration( 59 | Type contextType, Type entityType) 60 | { 61 | return allEntityConfigurations.FirstOrDefault(ec => 62 | ec.ContextType == contextType && ec.EntityType == entityType); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/EasyDynamo/Config/EntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Core; 3 | using EasyDynamo.Tools.Validators; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq.Expressions; 7 | 8 | namespace EasyDynamo.Config 9 | { 10 | public class EntityConfiguration : IEntityConfiguration 11 | where TContext : DynamoContext 12 | where TEntity : class 13 | { 14 | private long readCapacityUnits; 15 | private long writeCapacityUnits; 16 | 17 | protected internal EntityConfiguration() 18 | { 19 | this.ValidateOnSave = true; 20 | this.Properties = new List>(); 21 | this.Indexes = new HashSet(); 22 | this.IgnoredMembersNames = new HashSet(); 23 | this.IgnoredMembersExpressions = new HashSet>>(); 24 | this.ReadCapacityUnits = 1; 25 | this.WriteCapacityUnits = 1; 26 | } 27 | 28 | public Type ContextType => typeof(TContext); 29 | 30 | public Type EntityType => typeof(TEntity); 31 | 32 | public ICollection> Properties { get; } 33 | 34 | public string TableName { get; set; } 35 | 36 | public string HashKeyMemberName { get; set; } 37 | 38 | public long ReadCapacityUnits 39 | { 40 | get => this.readCapacityUnits; 41 | set 42 | { 43 | InputValidator.ThrowIfNotPositive(value,string.Format( 44 | ExceptionMessage.PositiveIntegerNeeded, nameof(this.ReadCapacityUnits))); 45 | 46 | this.readCapacityUnits = value; 47 | } 48 | } 49 | 50 | public long WriteCapacityUnits 51 | { 52 | get => this.writeCapacityUnits; 53 | set 54 | { 55 | InputValidator.ThrowIfNotPositive(value, string.Format( 56 | ExceptionMessage.PositiveIntegerNeeded, nameof(this.WriteCapacityUnits))); 57 | 58 | this.writeCapacityUnits = value; 59 | } 60 | } 61 | 62 | public Type HashKeyMemberType { get; set; } 63 | 64 | public bool ValidateOnSave { get; set; } 65 | 66 | public Expression> HashKeyMemberExpression { get; set; } 67 | 68 | public ISet Indexes { get; } 69 | 70 | public ISet IgnoredMembersNames { get; } 71 | 72 | public ISet>> IgnoredMembersExpressions { get; } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Tools/TableDropperTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using EasyDynamo.Exceptions; 4 | using EasyDynamo.Tools; 5 | using Moq; 6 | using System; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace EasyDynamo.Tests.Tools 12 | { 13 | public class TableDropperTests 14 | { 15 | private readonly Mock clientMock; 16 | private readonly TableDropper dropper; 17 | 18 | public TableDropperTests() 19 | { 20 | this.clientMock = new Mock(); 21 | this.dropper = new TableDropper(this.clientMock.Object); 22 | } 23 | 24 | [Fact] 25 | public async Task DropTableAsync_CallsClientWithCorrectTableName() 26 | { 27 | const string TableName = "Articles"; 28 | 29 | this.clientMock 30 | .Setup(cli => cli.DeleteTableAsync( 31 | It.Is(dtr => dtr.TableName == TableName), 32 | It.IsAny())) 33 | .ReturnsAsync(new DeleteTableResponse 34 | { 35 | HttpStatusCode = System.Net.HttpStatusCode.OK 36 | }) 37 | .Verifiable(); 38 | 39 | await this.dropper.DropTableAsync(TableName); 40 | 41 | this.clientMock.Verify(); 42 | } 43 | 44 | [Fact] 45 | public async Task DropTableAsync_ResponseFail_ThrowsException() 46 | { 47 | const string TableName = "Articles"; 48 | 49 | this.clientMock 50 | .Setup(cli => cli.DeleteTableAsync( 51 | It.IsAny(), 52 | It.IsAny())) 53 | .ReturnsAsync(new DeleteTableResponse 54 | { 55 | HttpStatusCode = System.Net.HttpStatusCode.BadRequest 56 | }); 57 | 58 | await Assert.ThrowsAsync( 59 | () => this.dropper.DropTableAsync(TableName)); 60 | } 61 | 62 | [Fact] 63 | public async Task DropTableAsync_ClientThrows_ThrowsException() 64 | { 65 | const string TableName = "Articles"; 66 | 67 | this.clientMock 68 | .Setup(cli => cli.DeleteTableAsync( 69 | It.IsAny(), 70 | It.IsAny())) 71 | .ThrowsAsync(new InvalidOperationException()); 72 | 73 | await Assert.ThrowsAsync( 74 | () => this.dropper.DropTableAsync(TableName)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Tools/Validators/EntityValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Config; 3 | using EasyDynamo.Exceptions; 4 | using EasyDynamo.Tests.Fakes; 5 | using EasyDynamo.Tools.Validators; 6 | using Moq; 7 | using System; 8 | using System.Collections.Generic; 9 | using Xunit; 10 | 11 | namespace EasyDynamo.Tests.Tools.Validators 12 | { 13 | public class EntityValidatorTests 14 | { 15 | private readonly EntityValidator validator; 16 | private readonly Mock> entityConfigMock; 17 | private readonly Mock entityConfigurationProviderMock; 18 | 19 | public EntityValidatorTests() 20 | { 21 | this.entityConfigurationProviderMock = new Mock(); 22 | this.entityConfigMock = new Mock>(); 23 | this.validator = new EntityValidator(this.entityConfigurationProviderMock.Object); 24 | 25 | this.entityConfigurationProviderMock 26 | .Setup(p => p.GetEntityConfiguration(It.IsAny(), It.IsAny())) 27 | .Returns(this.entityConfigMock.Object); 28 | } 29 | 30 | [Fact] 31 | public void Validate_NoPrimaryKey_ThrowsException() 32 | { 33 | var entity = new FakeEntity(); 34 | 35 | Assert.Throws( 36 | () => this.validator.Validate(typeof(FakeDynamoContext), entity)); 37 | } 38 | 39 | [Fact] 40 | public void Validate_HasPrimaryKeyRequiredMemberIsNull_ThrowsException() 41 | { 42 | this.entityConfigMock 43 | .SetupGet(c => c.Properties) 44 | .Returns(new List> 45 | { 46 | new PropertyConfiguration(nameof(FakeEntity.Content)) 47 | { 48 | IsRequired = true 49 | } 50 | }); 51 | 52 | var entity = new FakeEntity 53 | { 54 | Id = 1 55 | }; 56 | 57 | Assert.Throws( 58 | () => this.validator.Validate(typeof(FakeDynamoContext), entity)); 59 | } 60 | 61 | [Fact] 62 | public void Validate_MemberWithRequiredAttributeIsNull_ThrowsException() 63 | { 64 | var entity = new FakeEntity 65 | { 66 | Id = 1 67 | }; 68 | 69 | Assert.Throws( 70 | () => this.validator.Validate(typeof(FakeDynamoContext), entity)); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Factories/IndexConfigurationFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Tests.Fakes; 2 | using EasyDynamo.Tools; 3 | using System; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace EasyDynamo.Tests.Factories 8 | { 9 | public class IndexConfigurationFactoryTests 10 | { 11 | private readonly IndexConfigurationFactory factory; 12 | 13 | public IndexConfigurationFactoryTests() 14 | { 15 | this.factory = new IndexConfigurationFactory(); 16 | } 17 | 18 | [Fact] 19 | public void CreateIndexConfigByAttributes_EntityTypeIsNull_ThrowsException() 20 | { 21 | Assert.Throws( 22 | () => this.factory.CreateIndexConfigByAttributes(null)); 23 | } 24 | 25 | [Fact] 26 | public void CreateIndexConfigByAttributes_ValidEntityType_ReturnsConfigurationWithCorrectIndexName() 27 | { 28 | var configurations = this.factory.CreateIndexConfigByAttributes(typeof(FakeEntity)); 29 | var config = configurations.Single(); 30 | 31 | Assert.Equal(FakeEntity.IndexName, config.IndexName); 32 | } 33 | 34 | [Fact] 35 | public void CreateIndexConfigByAttributes_ValidEntityType_ReturnsConfigurationWithCorrectHashKeyName() 36 | { 37 | var configurations = this.factory.CreateIndexConfigByAttributes(typeof(FakeEntity)); 38 | var config = configurations.Single(); 39 | 40 | Assert.Equal(nameof(FakeEntity.Title), config.HashKeyMemberName); 41 | } 42 | 43 | [Fact] 44 | public void CreateIndexConfigByAttributes_ValidEntityType_ReturnsConfigurationWithCorrectHashKeyType() 45 | { 46 | var configurations = this.factory.CreateIndexConfigByAttributes(typeof(FakeEntity)); 47 | var config = configurations.Single(); 48 | 49 | Assert.Equal(typeof(string), config.HashKeyMemberType); 50 | } 51 | 52 | [Fact] 53 | public void CreateIndexConfigByAttributes_ValidEntityType_ReturnsConfigurationWithCorrectRangeKeyName() 54 | { 55 | var configurations = this.factory.CreateIndexConfigByAttributes(typeof(FakeEntity)); 56 | var config = configurations.Single(); 57 | 58 | Assert.Equal(nameof(FakeEntity.LastModified), config.RangeKeyMemberName); 59 | } 60 | 61 | [Fact] 62 | public void CreateIndexConfigByAttributes_ValidEntityType_ReturnsConfigurationWithCorrectRangeKeyType() 63 | { 64 | var configurations = this.factory.CreateIndexConfigByAttributes(typeof(FakeEntity)); 65 | var config = configurations.Single(); 66 | 67 | Assert.Equal(typeof(DateTime), config.RangeKeyMemberType); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/EasyDynamo/Factories/IndexConfigurationFactory.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using EasyDynamo.Abstractions; 3 | using EasyDynamo.Config; 4 | using EasyDynamo.Tools.Validators; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace EasyDynamo.Tools 11 | { 12 | public class IndexConfigurationFactory : IIndexConfigurationFactory 13 | { 14 | public IEnumerable CreateIndexConfigByAttributes( 15 | Type entityType) 16 | { 17 | InputValidator.ThrowIfNull(entityType); 18 | 19 | var configs = new List(); 20 | 21 | var hashAttributesByProperty = entityType 22 | .GetProperties() 23 | .Where(pi => pi.GetCustomAttribute< 24 | DynamoDBGlobalSecondaryIndexHashKeyAttribute>(true) != null) 25 | .Select(pi => new 26 | { 27 | PropertyInfo = pi, 28 | HashAttribute = pi.GetCustomAttribute< 29 | DynamoDBGlobalSecondaryIndexHashKeyAttribute>(true) 30 | }) 31 | .ToDictionary(kvp => kvp.PropertyInfo, kvp => kvp.HashAttribute); 32 | var rangeAttributesByProperty = entityType 33 | .GetProperties() 34 | .Where(pi => pi.GetCustomAttribute< 35 | DynamoDBGlobalSecondaryIndexRangeKeyAttribute>(true) != null) 36 | .Select(pi => new 37 | { 38 | PropertyInfo = pi, 39 | HashAttribute = pi.GetCustomAttribute< 40 | DynamoDBGlobalSecondaryIndexRangeKeyAttribute>(true) 41 | }) 42 | .ToDictionary(kvp => kvp.PropertyInfo, kvp => kvp.HashAttribute); 43 | 44 | foreach (var kvp in hashAttributesByProperty) 45 | { 46 | var indexName = kvp.Value.IndexNames.FirstOrDefault(); 47 | 48 | var currentConfig = new GlobalSecondaryIndexConfiguration 49 | { 50 | IndexName = indexName, 51 | ReadCapacityUnits = Constants.DefaultReadCapacityUnits, 52 | WriteCapacityUnits = Constants.DefaultWriteCapacityUnits, 53 | HashKeyMemberName = kvp.Key.Name, 54 | HashKeyMemberType = kvp.Key.PropertyType 55 | }; 56 | 57 | if (rangeAttributesByProperty.Any(a => a.Value.IndexNames?[0] == indexName)) 58 | { 59 | var rangeKeyProperty = rangeAttributesByProperty 60 | .First(rap => rap.Value.IndexNames[0] == indexName) 61 | .Key; 62 | currentConfig.RangeKeyMemberName = rangeKeyProperty.Name; 63 | currentConfig.RangeKeyMemberType = rangeKeyProperty.PropertyType; 64 | } 65 | 66 | configs.Add(currentConfig); 67 | } 68 | 69 | return configs; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/EasyDynamo/Config/DynamoContextOptions.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.DynamoDBv2; 3 | using Amazon.Extensions.NETCore.Setup; 4 | using EasyDynamo.Abstractions; 5 | using EasyDynamo.Attributes; 6 | using EasyDynamo.Core; 7 | using EasyDynamo.Exceptions; 8 | using EasyDynamo.Tools.Validators; 9 | using System; 10 | using System.Collections.Generic; 11 | 12 | namespace EasyDynamo.Config 13 | { 14 | [IgnoreAutoResolving] 15 | public class DynamoContextOptions : IDynamoContextOptions 16 | { 17 | protected internal DynamoContextOptions(Type contextType) 18 | { 19 | this.EnsureValidContextType(contextType); 20 | 21 | this.ContextType = contextType; 22 | this.TableNameByEntityTypes = new Dictionary(); 23 | this.AwsOptions = new AWSOptions(); 24 | } 25 | 26 | public Type ContextType { get; } 27 | 28 | public IDictionary TableNameByEntityTypes { get; } 29 | 30 | public string AccessKeyId { get; set; } 31 | 32 | public string SecretAccessKey { get; set; } 33 | 34 | public bool LocalMode { get; set; } 35 | 36 | public string ServiceUrl { get; set; } 37 | 38 | public RegionEndpoint RegionEndpoint { get; set; } 39 | 40 | public string Profile { get; set; } 41 | 42 | public DynamoDBEntryConversion Conversion { get; set; } 43 | 44 | public AWSOptions AwsOptions { get; set; } 45 | 46 | public void ValidateLocalMode() 47 | { 48 | if (!this.LocalMode) 49 | { 50 | return; 51 | } 52 | 53 | var requiredValuesProvided = !string.IsNullOrWhiteSpace(this.ServiceUrl) && 54 | !string.IsNullOrWhiteSpace(this.AccessKeyId) && 55 | !string.IsNullOrWhiteSpace(this.SecretAccessKey); 56 | 57 | if (!requiredValuesProvided) 58 | { 59 | throw new DynamoContextConfigurationException( 60 | $"When local mode is enabled the following values must be provided: " + 61 | $"{nameof(this.ServiceUrl)}, " + 62 | $"{nameof(this.AccessKeyId)}, " + 63 | $"{nameof(this.SecretAccessKey)}."); 64 | } 65 | } 66 | 67 | public void ValidateCloudMode() 68 | { 69 | if (this.LocalMode) 70 | { 71 | return; 72 | } 73 | 74 | var requiredValuesProvided = this.RegionEndpoint != null && 75 | !string.IsNullOrWhiteSpace(this.Profile); 76 | 77 | if (!requiredValuesProvided) 78 | { 79 | throw new DynamoContextConfigurationException( 80 | $"When cloud mode is enabled the following values must be provided: " + 81 | $"{nameof(this.RegionEndpoint)}, " + 82 | $"{nameof(this.Profile)}."); 83 | } 84 | } 85 | 86 | public void UseTableName(string tableName) where TEntity : class, new() 87 | { 88 | InputValidator.ThrowIfNullOrWhitespace( 89 | tableName, $"Parameter name cannot be empty: {nameof(tableName)}."); 90 | 91 | this.TableNameByEntityTypes[typeof(TEntity)] = tableName; 92 | } 93 | 94 | private void EnsureValidContextType(Type contextType) 95 | { 96 | if (typeof(DynamoContext).IsAssignableFrom(contextType)) 97 | { 98 | return; 99 | } 100 | 101 | throw new DynamoContextConfigurationException( 102 | $"{contextType.FullName} does not inherit from {nameof(DynamoContext)}."); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/EasyDynamo/Core/DatabaseFacade.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace EasyDynamo.Core 9 | { 10 | public class DatabaseFacade 11 | { 12 | private readonly IDynamoContextOptions options; 13 | private readonly ITableNameExtractor tableNameExtractor; 14 | private readonly ITableCreator tableCreator; 15 | private readonly ITableDropper tableDropper; 16 | 17 | public DatabaseFacade( 18 | ITableNameExtractor tableNameExtractor, 19 | ITableCreator tableCreator, 20 | ITableDropper tableDropper, 21 | IDynamoContextOptionsProvider optionsProvider, 22 | Type contextType) 23 | { 24 | this.options = optionsProvider.GetContextOptions(contextType); 25 | this.tableNameExtractor = tableNameExtractor; 26 | this.tableCreator = tableCreator; 27 | this.tableDropper = tableDropper; 28 | } 29 | 30 | /// 31 | /// Creates all tables (if not exist), declared as 32 | /// DynamoDbSet in the application's DynamoContext class. 33 | /// 34 | public async Task EnsureCreatedAsync( 35 | CancellationToken cancellationToken = default) 36 | { 37 | await Task.Run(async () => 38 | { 39 | var allTableNames = await this.tableNameExtractor.ExtractAllTableNamesAsync(); 40 | var newTablesByEntityType = new Dictionary(); 41 | 42 | foreach (var kvp in this.options.TableNameByEntityTypes) 43 | { 44 | var optionsTableName = kvp.Value; 45 | 46 | if (allTableNames.Contains(optionsTableName)) 47 | { 48 | continue; 49 | } 50 | 51 | var newTableName = await this.tableCreator 52 | .CreateTableAsync(this.options.ContextType, kvp.Key, kvp.Value); 53 | 54 | newTablesByEntityType[kvp.Key] = newTableName; 55 | } 56 | 57 | foreach (var kvp in newTablesByEntityType) 58 | { 59 | this.options.TableNameByEntityTypes[kvp.Key] = kvp.Value; 60 | } 61 | }, 62 | cancellationToken); 63 | } 64 | 65 | /// 66 | /// Drops all tables for the configured AmazonDynamoDB client. 67 | /// DO NOT use with profiles with access to production tables. 68 | /// 69 | public async Task EnsureDeletedAsync( 70 | CancellationToken cancellationToken = default(CancellationToken)) 71 | { 72 | var result = await Task.Run(async () => 73 | { 74 | try 75 | { 76 | var allTableNames = await this.tableNameExtractor.ExtractAllTableNamesAsync(); 77 | 78 | foreach (var kvp in this.options.TableNameByEntityTypes) 79 | { 80 | var tableName = kvp.Value; 81 | 82 | if (!allTableNames.Contains(tableName)) 83 | { 84 | continue; 85 | } 86 | 87 | await this.tableDropper.DropTableAsync(tableName); 88 | } 89 | 90 | return true; 91 | } 92 | catch 93 | { 94 | return false; 95 | } 96 | }, 97 | cancellationToken); 98 | 99 | return result; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /doc/configure-access.md: -------------------------------------------------------------------------------- 1 | ## How to configure the database access 2 | 3 | #### 1. Make your own database context class and inherit from EasyDynamo.Core.DynamoContext as below: 4 | 5 | ```csharp 6 | public class BlogDbContext : DynamoContext 7 | { 8 | public BlogDbContext(IServiceProvider serviceProvider) 9 | : base(serviceProvider) { } 10 | } 11 | ``` 12 | The IServiceProvider will be resolved by the MVC or you can pass your own if the application is not an ASP.NET app. 13 | 14 | #### 2. Make your model classes: 15 | 16 | ```csharp 17 | public class User 18 | { 19 | public string Id { get; set; } 20 | 21 | public string Username { get; set; } 22 | 23 | public string Email { get; set; } 24 | 25 | public string PasswordHash { get; set; } 26 | 27 | public string FirstName { get; set; } 28 | 29 | public string LastName { get; set; } 30 | 31 | public DateTime DateRegistered { get; set; } 32 | 33 | public DateTime LastActivity { get; set; } 34 | } 35 | ``` 36 | ```csharp 37 | public class Article 38 | { 39 | public string Id { get; set; } 40 | 41 | public string Title { get; set; } 42 | 43 | public string Content { get; set; } 44 | 45 | public DateTime CreatedOn { get; set; } 46 | 47 | public string AuthorId { get; set; } 48 | } 49 | ``` 50 | 51 | #### 3. Add all your models to your database context class as DynamoDbSets: 52 | 53 | ```csharp 54 | public class BlogDbContext : DynamoContext 55 | { 56 | public BlogDbContext(IServiceProvider serviceProvider) 57 | : base(serviceProvider) { } 58 | 59 | public DynamoDbSet Users { get; set; } 60 | 61 | public DynamoDbSet
Articles { get; set; } 62 | } 63 | ``` 64 | 65 | #### 4. Add your database context class to the service resolver in Startup.cs: 66 | 67 | ```csharp 68 | public void ConfigureServices(IServiceCollection services) 69 | { 70 | services.AddDynamoContext(this.Configuration); 71 | 72 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 73 | } 74 | ``` 75 | 76 | #### 5. Add whatever options you want to the dynamo configuration, you can hardcode them or you can use the application configuration files or environment variables: 77 | 78 | ```csharp 79 | services.AddDynamoContext(this.Configuration, options => 80 | { 81 | options.Profile = "MyAmazonProfile"; 82 | options.RegionEndpoint = RegionEndpoint.USEast1; 83 | options.AccessKeyId = Environment.GetEnvironmentVariable("AccessKey"); 84 | options.SecretAccessKey = this.Configuration.GetValue("Credentials:SecretKey"); 85 | }); 86 | ``` 87 | 88 | ##### 5.1. Another way to add configuration and keep startup.cs thin: 89 | Just override OnConfiguring method of your database context class and specify your options there. Of course you can hardcode them or you can use the application configuration files or environment variables. 90 | 91 | ```csharp 92 | public class BlogDbContext : DynamoContext 93 | { 94 | public BlogDbContext(IServiceProvider serviceProvider) 95 | : base(serviceProvider) { } 96 | 97 | public DynamoDbSet Users { get; set; } 98 | 99 | public DynamoDbSet
Articles { get; set; } 100 | 101 | protected override void OnConfiguring( 102 | DynamoContextOptionsBuilder builder, IConfiguration configuration) 103 | { 104 | builder.UseAccessKeyId(Environment.GetEnvironmentVariable("AccessKey")); 105 | builder.UseSecretAccessKey(configuration.GetValue("AWS:Credentials:SecretKey")); 106 | builder.UseRegionEndpoint(RegionEndpoint.USEast1); 107 | 108 | base.OnConfiguring(builder, configuration); 109 | } 110 | } 111 | ``` 112 | 113 | You are ready to use your database context class wherever you want: 114 | 115 | ```csharp 116 | public class HomeController : Controller 117 | { 118 | private readonly BlogDbContext context; 119 | 120 | public HomeController(BlogDbContext context) 121 | { 122 | this.context = context; 123 | } 124 | 125 | public async Task Index() 126 | { 127 | var articles = await this.context.Articles.GetAsync(); 128 | 129 | return View(articles); 130 | } 131 | } 132 | ``` -------------------------------------------------------------------------------- /src/EasyDynamo/Builders/DynamoContextOptionsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.DynamoDBv2; 3 | using EasyDynamo.Abstractions; 4 | using EasyDynamo.Exceptions; 5 | using EasyDynamo.Tools.Validators; 6 | using System; 7 | 8 | namespace EasyDynamo.Builders 9 | { 10 | public class DynamoContextOptionsBuilder 11 | { 12 | private readonly IDynamoContextOptions options; 13 | 14 | protected internal DynamoContextOptionsBuilder(IDynamoContextOptions options) 15 | { 16 | this.options = options; 17 | } 18 | 19 | /// 20 | /// Adds a specific name for the table corresponding to the given entity. 21 | /// 22 | /// 23 | public DynamoContextOptionsBuilder UseTableName(string tableName) 24 | where TEntity : class, new() 25 | { 26 | InputValidator.ThrowIfNullOrWhitespace(tableName); 27 | 28 | this.options.UseTableName(tableName); 29 | 30 | return this; 31 | } 32 | 33 | /// 34 | /// Adds a specific access key to the dynamo client's credentials 35 | /// 36 | /// 37 | public DynamoContextOptionsBuilder UseAccessKeyId(string accessKey) 38 | { 39 | InputValidator.ThrowIfNullOrWhitespace(accessKey); 40 | 41 | this.options.AccessKeyId = accessKey; 42 | 43 | return this; 44 | } 45 | 46 | /// 47 | /// Adds a specific access secret to the dynamo client's credentials 48 | /// 49 | /// 50 | public DynamoContextOptionsBuilder UseSecretAccessKey(string accessSecret) 51 | { 52 | InputValidator.ThrowIfNullOrWhitespace(accessSecret); 53 | 54 | this.options.SecretAccessKey = accessSecret; 55 | 56 | return this; 57 | } 58 | 59 | /// 60 | /// Use a local instance of a dynamoDb on a given service url. For example: "http://localhost:8013". 61 | /// 62 | /// Required parameter. 63 | /// 64 | public DynamoContextOptionsBuilder UseLocalMode(string serviceUrl) 65 | { 66 | InputValidator.ThrowIfNullOrWhitespace( 67 | serviceUrl, $"{nameof(serviceUrl)} must be provided."); 68 | 69 | options.ServiceUrl = serviceUrl ?? options.ServiceUrl; 70 | 71 | return this; 72 | } 73 | 74 | /// 75 | /// Adds a specific service url to the configuration. For example: "http://localhost:8013". 76 | /// 77 | /// Required parameter. 78 | /// 79 | public DynamoContextOptionsBuilder UseServiceUrl(string serviceUrl) 80 | { 81 | InputValidator.ThrowIfNullOrWhitespace( 82 | serviceUrl, $"{nameof(serviceUrl)} must be provided."); 83 | 84 | this.options.ServiceUrl = serviceUrl; 85 | 86 | return this; 87 | } 88 | 89 | /// 90 | /// Adds a specific region to the configuration. 91 | /// 92 | /// Required parameter. 93 | /// 94 | public DynamoContextOptionsBuilder UseRegionEndpoint(RegionEndpoint region) 95 | { 96 | InputValidator.ThrowIfNull( 97 | region, $"{nameof(region)} must be provided."); 98 | 99 | this.options.RegionEndpoint = region; 100 | 101 | return this; 102 | } 103 | 104 | /// 105 | /// Adds a schema fully supporting 2014 L, M, BOOL, NULL additions. 106 | /// 107 | public DynamoContextOptionsBuilder UseEntryConversionV2() 108 | { 109 | this.options.Conversion = DynamoDBEntryConversion.V2; 110 | 111 | return this; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Builders/ModelBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Builders; 3 | using EasyDynamo.Config; 4 | using EasyDynamo.Tests.Fakes; 5 | using Moq; 6 | using System; 7 | using Xunit; 8 | 9 | namespace EasyDynamo.Tests.Builders 10 | { 11 | public class ModelBuilderTests 12 | { 13 | private readonly Mock optionsMock; 14 | private readonly ModelBuilderFake builder; 15 | 16 | public ModelBuilderTests() 17 | { 18 | this.optionsMock = new Mock(); 19 | this.builder = new ModelBuilderFake(optionsMock.Object); 20 | } 21 | 22 | [Fact] 23 | public void ApplyConfiguration_ConfigurationIsNull_ThrowsException() 24 | { 25 | Assert.Throws( 26 | () => this.builder.ApplyConfiguration(default(EntityTypeConfugurationFake))); 27 | } 28 | 29 | [Fact] 30 | public void ApplyConfiguration_ValidConfiguration_InvokeConfigure() 31 | { 32 | var configuration = new EntityTypeConfugurationFake(); 33 | 34 | this.builder.ApplyConfiguration(configuration); 35 | 36 | Assert.True(configuration.ConfigureInvoked); 37 | } 38 | 39 | [Fact] 40 | public void ApplyConfiguration_ValidConfiguration_InvokeConfigureWithCorrectType() 41 | { 42 | var configuration = new EntityTypeConfugurationFake(); 43 | 44 | this.builder.ApplyConfiguration(configuration); 45 | 46 | Assert.Equal( 47 | typeof(EntityTypeBuilder), 48 | configuration.ConfigureInvokedWithBuilderType); 49 | } 50 | 51 | [Fact] 52 | public void ApplyConfiguration_ValidConfiguration_ReturnsSameInstanceOfBuilder() 53 | { 54 | var configuration = new EntityTypeConfugurationFake(); 55 | 56 | var returnedBuilder = this.builder.ApplyConfiguration(configuration); 57 | 58 | Assert.Equal(this.builder, returnedBuilder); 59 | } 60 | 61 | [Fact] 62 | public void Entity_ReturnsEntityBuilderOfCorrectType() 63 | { 64 | var entBuilder = this.builder.Entity(); 65 | 66 | Assert.Equal( 67 | typeof(EntityTypeBuilder), 68 | entBuilder.GetType()); 69 | } 70 | 71 | [Fact] 72 | public void Entity_AddsCorrectKeyValuePairInTheMap() 73 | { 74 | this.builder.Entity(); 75 | 76 | Assert.Single(this.builder.EntityConfigurationsFromBase); 77 | Assert.Contains( 78 | this.builder.EntityConfigurationsFromBase, 79 | kvp => kvp.Key == typeof(FakeEntity)); 80 | Assert.Contains( 81 | this.builder.EntityConfigurationsFromBase, 82 | kvp => kvp.Value.GetType() == typeof(EntityConfiguration)); 83 | } 84 | 85 | [Fact] 86 | public void Entity_BuildActionIsNull_ThrowsException() 87 | { 88 | Assert.Throws( 89 | () => this.builder.Entity(default(Action>))); 90 | } 91 | 92 | [Fact] 93 | public void Entity_ValidBuildAction_InvokeAction() 94 | { 95 | Action> buildAction = e => 96 | { 97 | e.HasPrimaryKey(ent => ent.Id); 98 | }; 99 | 100 | this.builder.Entity(buildAction); 101 | 102 | Assert.Single(buildAction.GetInvocationList()); 103 | } 104 | 105 | [Fact] 106 | public void Entity_ValidBuildAction_ReturnsSameInstanceOfBuilder() 107 | { 108 | Action> buildAction = e => 109 | { 110 | e.HasPrimaryKey(ent => ent.Id); 111 | }; 112 | 113 | var returnedBuilder = this.builder.Entity(buildAction); 114 | 115 | Assert.Equal(this.builder, returnedBuilder); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/EasyDynamo/Builders/ModelBuilder.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Config; 3 | using EasyDynamo.Core; 4 | using EasyDynamo.Tools.Validators; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace EasyDynamo.Builders 9 | { 10 | public class ModelBuilder 11 | { 12 | private readonly IDynamoContextOptions contextOptions; 13 | private readonly Dictionary entityBuildersByEntityType; 14 | 15 | protected internal ModelBuilder(IDynamoContextOptions contextOptions) 16 | { 17 | this.contextOptions = contextOptions; 18 | this.entityBuildersByEntityType = new Dictionary(); 19 | this.EntityConfigurations = new Dictionary(); 20 | } 21 | 22 | protected internal IDictionary EntityConfigurations { get; } 23 | 24 | /// 25 | /// Applies a specific configuration for a given entity. 26 | /// Usage: Create a class that implements IEntityTypeConfiguration 27 | /// and build all configurations in the Configure method. 28 | /// Then call ApplyConfiguration with a new instance of that implementation class. 29 | /// 30 | public ModelBuilder ApplyConfiguration( 31 | IEntityTypeConfiguration configuration) 32 | where TContext : DynamoContext 33 | where TEntity : class, new() 34 | { 35 | InputValidator.ThrowIfNull(configuration, "configuration connot be null."); 36 | 37 | var entityBuilder = this.GetEntityBuilder(); 38 | 39 | configuration.Configure(entityBuilder); 40 | 41 | return this; 42 | } 43 | 44 | /// 45 | /// Returns a builder for a specified entity type. 46 | /// 47 | public IEntityTypeBuilder Entity() 48 | where TContext : DynamoContext 49 | where TEntity : class, new() 50 | { 51 | return this.GetEntityBuilder(); 52 | } 53 | 54 | /// 55 | /// Applies a specific configuration for a given entity. 56 | /// Insert the entity configuration in the buildAction parameter. 57 | /// 58 | public ModelBuilder Entity( 59 | Action> buildAction) 60 | where TContext : DynamoContext 61 | where TEntity : class, new() 62 | { 63 | InputValidator.ThrowIfNull(buildAction, "buildAction cannot be null."); 64 | 65 | var entityBuilder = this.GetEntityBuilder(); 66 | 67 | buildAction(entityBuilder); 68 | 69 | return this; 70 | } 71 | 72 | private IEntityTypeBuilder GetEntityBuilder() 73 | where TContext : DynamoContext 74 | where TEntity : class, new() 75 | { 76 | var entityConfig = default(EntityConfiguration); 77 | 78 | if (this.EntityConfigurations.ContainsKey(typeof(TEntity))) 79 | { 80 | entityConfig = (EntityConfiguration) 81 | this.EntityConfigurations[typeof(TEntity)]; 82 | } 83 | 84 | if (entityConfig == null) 85 | { 86 | entityConfig = new EntityConfiguration(); 87 | 88 | this.EntityConfigurations[typeof(TEntity)] = entityConfig; 89 | } 90 | 91 | var entityBuilder = default(EntityTypeBuilder); 92 | 93 | if (this.entityBuildersByEntityType.ContainsKey(typeof(TEntity))) 94 | { 95 | entityBuilder = (EntityTypeBuilder) 96 | this.entityBuildersByEntityType[typeof(TEntity)]; 97 | } 98 | 99 | if (entityBuilder == null) 100 | { 101 | entityBuilder = new EntityTypeBuilder( 102 | entityConfig, this.contextOptions); 103 | 104 | this.entityBuildersByEntityType[typeof(TEntity)] = entityBuilder; 105 | } 106 | 107 | return entityBuilder; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/Validators/EntityValidator.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using EasyDynamo.Abstractions; 3 | using EasyDynamo.Exceptions; 4 | using EasyDynamo.Extensions; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel.DataAnnotations; 8 | using System.Linq; 9 | 10 | namespace EasyDynamo.Tools.Validators 11 | { 12 | public class EntityValidator : IEntityValidator 13 | { 14 | private readonly IEntityConfigurationProvider entityConfigurationProvider; 15 | 16 | public EntityValidator(IEntityConfigurationProvider entityConfigurationProvider) 17 | { 18 | this.entityConfigurationProvider = entityConfigurationProvider; 19 | } 20 | 21 | public IEnumerable GetValidationResults(Type contextType, TEntity entity) 22 | where TEntity : class 23 | { 24 | var validationContext = new ValidationContext(entity); 25 | var validationResults = new List(); 26 | var isValid = Validator.TryValidateObject( 27 | entity, validationContext, validationResults, true); 28 | 29 | return validationResults; 30 | } 31 | 32 | public void Validate(Type contextType, TEntity entity) where TEntity : class 33 | { 34 | this.ValidatePrimaryKey(contextType, entity); 35 | 36 | this.ValidateByConfiguration(contextType, entity); 37 | 38 | this.ValidateByAttributes(contextType, entity); 39 | } 40 | 41 | private void ValidateByConfiguration(Type contextType, TEntity entity) 42 | where TEntity : class 43 | { 44 | var errors = new List(); 45 | var entityConfig = this.entityConfigurationProvider.GetEntityConfiguration( 46 | contextType, typeof(TEntity)) as IEntityConfiguration; 47 | 48 | foreach (var memberConfig in entityConfig.Properties) 49 | { 50 | if (!memberConfig.IsRequired) 51 | { 52 | continue; 53 | } 54 | 55 | if (entityConfig.IgnoredMembersNames.Contains(memberConfig.MemberName)) 56 | { 57 | continue; 58 | } 59 | 60 | var propertyInfo = typeof(TEntity) 61 | .GetProperty(memberConfig.MemberName); 62 | var propertyValue = propertyInfo.GetValue(entity); 63 | 64 | if (propertyValue != null) 65 | { 66 | continue; 67 | } 68 | 69 | errors.Add($"{memberConfig.MemberName} is required."); 70 | } 71 | 72 | if (errors.Count == 0) 73 | { 74 | return; 75 | } 76 | 77 | throw new EntityValidationFailedException(errors.JoinByNewLine()); 78 | } 79 | 80 | private void ValidateByAttributes(Type contextType, TEntity entity) 81 | where TEntity : class 82 | { 83 | var errors = this.GetValidationResults(contextType, entity) 84 | .Select(vr => vr.ErrorMessage); 85 | 86 | if (errors.Any()) 87 | { 88 | throw new EntityValidationFailedException(errors.JoinByNewLine()); 89 | } 90 | } 91 | 92 | private void ValidatePrimaryKey(Type contextType, TEntity entity) 93 | where TEntity : class 94 | { 95 | var entityConfig = this.entityConfigurationProvider.GetEntityConfiguration( 96 | contextType, typeof(TEntity)) as IEntityConfiguration; 97 | var hashKeyType = entityConfig?.HashKeyMemberType; 98 | var hashKeyTypeDefaultValue = hashKeyType?.GetDefaultValue(); 99 | var hashKeyFunction = entityConfig?.HashKeyMemberExpression?.Compile(); 100 | var hashKeyMember = entity 101 | .GetType() 102 | .GetProperties() 103 | .FirstOrDefault(pi => pi 104 | .GetCustomAttributes(false) 105 | .Any(attr => attr.GetType() == typeof(DynamoDBHashKeyAttribute))); 106 | var hashKeyExist = 107 | (!hashKeyFunction?.Invoke(entity)?.Equals(hashKeyTypeDefaultValue) ?? false) || 108 | hashKeyMember?.GetValue(entity) != null; 109 | 110 | if (hashKeyExist) 111 | { 112 | return; 113 | } 114 | 115 | throw new EntityValidationFailedException($"Hash key {hashKeyMember?.Name} not set."); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Factories/AttributeDefinitionFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using EasyDynamo.Config; 3 | using EasyDynamo.Factories; 4 | using EasyDynamo.Tests.Fakes; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace EasyDynamo.Tests.Factories 11 | { 12 | public class AttributeDefinitionFactoryTests 13 | { 14 | private const string HashKeyMemberName = "Id"; 15 | private readonly ScalarAttributeType HashKeyAttributeType = ScalarAttributeType.S; 16 | private readonly AttributeDefinitionFactory factory; 17 | 18 | public AttributeDefinitionFactoryTests() 19 | { 20 | this.factory = new AttributeDefinitionFactory(); 21 | } 22 | 23 | [Fact] 24 | public void CreateAttributeDefinitions_SetPrimaryKeyNameCorrectly() 25 | { 26 | var definitions = this.factory.CreateAttributeDefinitions( 27 | HashKeyMemberName, HashKeyAttributeType, null); 28 | 29 | Assert.Contains(definitions, d => d.AttributeName == HashKeyMemberName); 30 | } 31 | 32 | [Fact] 33 | public void CreateAttributeDefinitions_SetPrimaryKeyTypeCorrectly() 34 | { 35 | var definitions = this.factory.CreateAttributeDefinitions( 36 | HashKeyMemberName, HashKeyAttributeType, null); 37 | 38 | Assert.Contains(definitions, d => d.AttributeType == HashKeyAttributeType); 39 | } 40 | 41 | [Fact] 42 | public void CreateAttributeDefinitions_PrimaryKeyAttributeTypeIsNull_DoesNotAddDefinition() 43 | { 44 | var definitions = this.factory.CreateAttributeDefinitions( 45 | HashKeyMemberName, null, null); 46 | 47 | Assert.Empty(definitions); 48 | } 49 | 50 | [Theory] 51 | [InlineData(null)] 52 | [InlineData("")] 53 | [InlineData(" ")] 54 | public void CreateAttributeDefinitions_PrimaryKeyNameIsEmpty_DoesNotAddDefinition(string empty) 55 | { 56 | var definitions = this.factory.CreateAttributeDefinitions( 57 | empty, HashKeyAttributeType, null); 58 | 59 | Assert.Empty(definitions); 60 | } 61 | 62 | [Fact] 63 | public void CreateAttributeDefinitions_GsiConfigurationPassed_CreateDefinitionWithCorrectHashKeyDefinition() 64 | { 65 | var gsiConfig = new GlobalSecondaryIndexConfiguration 66 | { 67 | HashKeyMemberName = nameof(FakeEntity.Title), 68 | HashKeyMemberType = typeof(string), 69 | IndexName = "GSI_Entities_Title_LAstModified", 70 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 71 | RangeKeyMemberType = typeof(DateTime), 72 | ReadCapacityUnits = 3, 73 | WriteCapacityUnits = 5 74 | }; 75 | var gsiConfigs = new List { gsiConfig }; 76 | var definitions = this.factory.CreateAttributeDefinitions(null, null, gsiConfigs); 77 | 78 | Assert.Contains(definitions, d => 79 | d.AttributeName == gsiConfig.HashKeyMemberName && 80 | d.AttributeType == ScalarAttributeType.S); 81 | } 82 | 83 | [Fact] 84 | public void CreateAttributeDefinitions_GsiConfigurationPassed_CreateDefinitionWithCorrectHashKeyType() 85 | { 86 | var gsiConfig = new GlobalSecondaryIndexConfiguration 87 | { 88 | HashKeyMemberName = nameof(FakeEntity.Title), 89 | HashKeyMemberType = typeof(string), 90 | IndexName = "GSI_Entities_Title_LAstModified", 91 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 92 | RangeKeyMemberType = typeof(DateTime), 93 | ReadCapacityUnits = 3, 94 | WriteCapacityUnits = 5 95 | }; 96 | var gsiConfigs = new List { gsiConfig }; 97 | var definitions = this.factory.CreateAttributeDefinitions(null, null, gsiConfigs); 98 | 99 | Assert.Contains(definitions, d => 100 | d.AttributeName == gsiConfig.RangeKeyMemberName && 101 | d.AttributeType == ScalarAttributeType.S); 102 | } 103 | 104 | [Theory] 105 | [InlineData(2, 4)] 106 | [InlineData(5, 10)] 107 | [InlineData(8, 16)] 108 | [InlineData(32, 64)] 109 | [InlineData(200, 400)] 110 | public void CreateAttributeDefinitions_GsiConfigurationPassed_CreateCorrectCountOfDefinitions( 111 | int gsiConfigsCount, int expectedCount) 112 | { 113 | var gsiConfigs = Enumerable.Range(1, gsiConfigsCount) 114 | .Select(index => new GlobalSecondaryIndexConfiguration 115 | { 116 | HashKeyMemberName = $"Hash-{index}", 117 | HashKeyMemberType = typeof(string), 118 | IndexName = $"Index_{index}", 119 | RangeKeyMemberName = $"Range-{index}", 120 | RangeKeyMemberType = typeof(int) 121 | }); 122 | var definitions = this.factory.CreateAttributeDefinitions(null, null, gsiConfigs); 123 | 124 | Assert.Equal(expectedCount, definitions.Count()); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /doc/dynamo-context.md: -------------------------------------------------------------------------------- 1 | ### How to work with your database context class 2 | 3 | Every DynamoDbSet in your database context class is a wrapper around Amazon.DynamoDBv2.DataModel.IDynamoDBContext class. Every DynamoDbSet has basic methods for CRUD operations as well as for filter/scan operations. You can use them as well as the Base property that gives you an access to the Amazon.DynamoDBv2.DataModel.IDynamoDBContext implementation. 4 | 5 | #### 1. Get operations: 6 | ##### 1.1. Get all items from a table using GetAsync() method: 7 | ```csharp 8 | public class ArticleService : IArticleService 9 | { 10 | private readonly BlogDbContext context; 11 | 12 | public ArticleService(BlogDbContext context) 13 | { 14 | this.context = context; 15 | } 16 | 17 | public async Task> GetArticlesAsync() 18 | { 19 | return await context.Articles.GetAsync(); 20 | } 21 | } 22 | ``` 23 | ##### 1.2. Get an item by primary key: 24 | ```csharp 25 | public async Task
GetArticleAsync(string id) 26 | { 27 | return await context.Articles.GetAsync(id); 28 | } 29 | ``` 30 | ##### 1.3. Get an item by primary key and range key: 31 | ```csharp 32 | public async Task
GetArticleAsync(string primaryKey, DateTime rangeKey) 33 | { 34 | return await context.Articles.GetAsync(primaryKey, rangeKey); 35 | } 36 | ``` 37 | ##### 1.4. Get a paginated set of items: 38 | You should cache the pagination token from the response and pass it to the next call in order to retrieve the next set of items. 39 | ```csharp 40 | public async Task> GetNextPageAsync(int itemsPerPage, string paginationToken) 41 | { 42 | var response = await context.Articles.GetAsync(itemsPerPage, paginationToken); 43 | 44 | return response.NextResultSet; 45 | } 46 | ``` 47 | #### 2. Create operations: 48 | ##### 2.1. Add a new item in a table using AddAsync() method: 49 | If an entity with the same primary key already exist an exception will be thrown! 50 | ```csharp 51 | public async Task CreateAsync(Article article) 52 | { 53 | await this.context.Articles.AddAsync(article); 54 | } 55 | ``` 56 | ##### 2.2. Add or update an item using SaveAsync() method: 57 | If an entity with the same primary key already exist it will be updated, otherwise will be created. 58 | ```csharp 59 | public async Task AddOrUpdateAsync(Article article) 60 | { 61 | await this.context.Articles.SaveAsync(article); 62 | } 63 | ``` 64 | ##### 2.3. Add/update multiple items using SaveManyAsync() method: 65 | If an entity with the same primary key already exist it will be updated, otherwise will be created. 66 | ```csharp 67 | public async Task AddOrUpdateManyAsync(IEnumerable
articles) 68 | { 69 | await this.context.Articles.SaveManyAsync(articles); 70 | } 71 | ``` 72 | #### 3. Update operations 73 | ##### 3.1. Update an item using UpdateAsync() method: 74 | If an entity with the same primary key does not exist an exception will be thrown! 75 | ```csharp 76 | public async Task UpdateAsync(Article article) 77 | { 78 | await this.context.Articles.UpdateAsync(article); 79 | } 80 | ``` 81 | #### 4. Delete operations 82 | ##### 4.1. Delete an item using RemoveAsync(item) method: 83 | ```csharp 84 | public async Task DeleteAsync(Article article) 85 | { 86 | await this.context.Articles.RemoveAsync(article); 87 | } 88 | ``` 89 | ##### 4.2. Delete an item by primary key using RemoveAsync(id) method: 90 | ```csharp 91 | public async Task DeleteAsync(string primaryKey) 92 | { 93 | await this.context.Articles.RemoveAsync(primaryKey); 94 | } 95 | ``` 96 | #### 5. Filter/Query operations 97 | ##### 5.1. Filter items by predicate expression using FilterAsync(expression) method: 98 | Warning: Can be a very slow operation when using over a big table. 99 | ```csharp 100 | public async Task> GetLatestByTitleTermAsync(string searchTerm) 101 | { 102 | return await this.context.Articles.FilterAsync( 103 | a => a.Title.Contains(searchTerm) && a.CreatedOn > DateTime.UtcNow.AddYears(-1)); 104 | } 105 | ``` 106 | ##### 5.2. Filter items by member match: 107 | If there is an index with hash key the given property, the query operation will be made against that index. 108 | ```csharp 109 | public async Task> GetAllByTitleMatchAsync(string title) 110 | { 111 | return await this.context.Articles.FilterAsync( 112 | a => a.Title, ScanOperator.Contains, title); 113 | } 114 | ``` 115 | ##### 5.3. Filter items by member name and value: 116 | Query operation against an index. If index is not passed, the first index with hash key the given property found will be used. 117 | ```csharp 118 | public async Task> FilterByTitle(string title) 119 | { 120 | return await this.context.Articles.FilterAsync( 121 | nameof(Article.Title), title, "gsi_articles_title"); 122 | } 123 | ``` 124 | #### 6. Make other operations using the Base property 125 | You have an access to the wrapped Amazon.DynamoDBv2.DataModel.IDynamoDBContext via Base property in each DynamoDbSet you declared in the database context class. 126 | Examples: 127 | ```csharp 128 | public string GetTableName() 129 | { 130 | var tableInfo = this.context.Articles.Base.GetTargetTable
(); 131 | 132 | return tableInfo.TableName; 133 | } 134 | ``` 135 | ```csharp 136 | public async Task> GetAllAsync() 137 | { 138 | var batchGet = this.context.Articles.Base.CreateBatchGet
(); 139 | 140 | await batchGet.ExecuteAsync(); 141 | 142 | return batchGet.Results; 143 | } 144 | ``` -------------------------------------------------------------------------------- /src/EasyDynamo/Tools/TableCreator.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.DataModel; 3 | using Amazon.DynamoDBv2.Model; 4 | using EasyDynamo.Abstractions; 5 | using EasyDynamo.Config; 6 | using EasyDynamo.Exceptions; 7 | using EasyDynamo.Extensions; 8 | using EasyDynamo.Tools.Validators; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Threading.Tasks; 14 | 15 | namespace EasyDynamo.Tools 16 | { 17 | public class TableCreator : ITableCreator 18 | { 19 | private readonly IAmazonDynamoDB client; 20 | private readonly IIndexFactory indexFactory; 21 | private readonly IAttributeDefinitionFactory attributeDefinitionFactory; 22 | private readonly IIndexConfigurationFactory indexConfigurationFactory; 23 | private readonly IEntityConfigurationProvider entityConfigurationProvider; 24 | 25 | public TableCreator( 26 | IAmazonDynamoDB client, 27 | IIndexFactory indexFactory, 28 | IAttributeDefinitionFactory attributeDefinitionFactory, 29 | IIndexConfigurationFactory indexConfigurationFactory, 30 | IEntityConfigurationProvider entityConfigurationProvider) 31 | { 32 | this.client = client; 33 | this.indexFactory = indexFactory; 34 | this.attributeDefinitionFactory = attributeDefinitionFactory; 35 | this.indexConfigurationFactory = indexConfigurationFactory; 36 | this.entityConfigurationProvider = entityConfigurationProvider; 37 | } 38 | 39 | public async Task CreateTableAsync( 40 | Type contextType, Type entityType, string tableName) 41 | { 42 | InputValidator.ThrowIfAnyNullOrWhitespace(entityType, tableName); 43 | 44 | var dynamoDbTableAttribute = entityType.GetCustomAttribute(true); 45 | tableName = dynamoDbTableAttribute?.TableName ?? tableName; 46 | var hashKeyMember = entityType 47 | .GetProperties() 48 | .SingleOrDefault(pi => pi.GetCustomAttributes() 49 | .Any(attr => attr.GetType() == typeof(DynamoDBHashKeyAttribute))); 50 | var hashKeyMemberAttribute = entityType 51 | .GetProperty(hashKeyMember?.Name ?? string.Empty) 52 | ?.GetCustomAttribute(true); 53 | var hashKeyMemberType = entityType 54 | .GetProperty(hashKeyMember?.Name ?? string.Empty) 55 | ?.PropertyType; 56 | var entityConfigRequired = hashKeyMember == null; 57 | var entityConfig = this.entityConfigurationProvider 58 | .TryGetEntityConfiguration(contextType, entityType); 59 | 60 | if (entityConfigRequired && entityConfig == null) 61 | { 62 | throw new DynamoContextConfigurationException(string.Format( 63 | ExceptionMessage.EntityConfigurationNotFound, entityType.FullName)); 64 | } 65 | 66 | var hashKeyMemberName = entityConfig?.HashKeyMemberName 67 | ?? hashKeyMemberAttribute.AttributeName 68 | ?? hashKeyMember.Name; 69 | var hashKeyMemberAttributeType = new ScalarAttributeType( 70 | hashKeyMemberType != null 71 | ? Constants.AttributeTypesMap[hashKeyMemberType] 72 | : Constants.AttributeTypesMap[entityConfig.HashKeyMemberType]); 73 | var readCapacityUnits = entityConfig?.ReadCapacityUnits 74 | ?? Constants.DefaultReadCapacityUnits; 75 | var writeCapacityUnits = entityConfig?.WriteCapacityUnits 76 | ?? Constants.DefaultWriteCapacityUnits; 77 | var gsisConfiguration = (entityConfig?.Indexes?.Count ?? 0) > 0 78 | ? entityConfig.Indexes 79 | : this.indexConfigurationFactory.CreateIndexConfigByAttributes(entityType); 80 | 81 | var request = new CreateTableRequest 82 | { 83 | TableName = tableName, 84 | AttributeDefinitions = this.attributeDefinitionFactory.CreateAttributeDefinitions( 85 | hashKeyMemberName, hashKeyMemberAttributeType, gsisConfiguration).ToList(), 86 | KeySchema = new List 87 | { 88 | new KeySchemaElement(hashKeyMemberName, KeyType.HASH) 89 | }, 90 | ProvisionedThroughput = new ProvisionedThroughput 91 | { 92 | ReadCapacityUnits = readCapacityUnits, 93 | WriteCapacityUnits = writeCapacityUnits 94 | }, 95 | GlobalSecondaryIndexes = gsisConfiguration.Count() == 0 96 | ? null 97 | : this.indexFactory.CreateRequestIndexes(gsisConfiguration).ToList() 98 | }; 99 | 100 | try 101 | { 102 | var response = await this.client.CreateTableAsync(request); 103 | 104 | if (!response.HttpStatusCode.IsSuccessful()) 105 | { 106 | throw new CreateTableFailedException( 107 | response.ResponseMetadata.Metadata.JoinByNewLine()); 108 | } 109 | 110 | return request.TableName; 111 | } 112 | catch (Exception ex) 113 | { 114 | throw new CreateTableFailedException("Failed to create a table.", ex); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/EasyDynamo/Core/DynamoContext.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2.DataModel; 2 | using EasyDynamo.Abstractions; 3 | using EasyDynamo.Attributes; 4 | using EasyDynamo.Builders; 5 | using Microsoft.Extensions.Configuration; 6 | using System; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace EasyDynamo.Core 11 | { 12 | public abstract class DynamoContext 13 | { 14 | private readonly IDependencyResolver dependencyResolver; 15 | private readonly IDynamoContextOptions options; 16 | 17 | protected DynamoContext(IDependencyResolver dependencyResolver) 18 | { 19 | this.dependencyResolver = dependencyResolver; 20 | this.options = this.dependencyResolver 21 | .GetDependency() 22 | .GetContextOptions(this.GetType()); 23 | this.Database = this.GetDatabaseFacadeInstance(); 24 | this.ListAllTablesByDbSets(); 25 | this.InstantiateAllSets(); 26 | } 27 | 28 | public DatabaseFacade Database { get; } 29 | 30 | /// 31 | /// Use to configure the Amazon.DynamoDBv2.IAmazonDynamoDB client. 32 | /// 33 | protected virtual void OnConfiguring( 34 | DynamoContextOptionsBuilder builder, 35 | IConfiguration configuration) 36 | { 37 | return; 38 | } 39 | 40 | /// 41 | /// Use to configure the database models. 42 | /// 43 | protected virtual void OnModelCreating(ModelBuilder builder, IConfiguration configuration) 44 | { 45 | return; 46 | } 47 | 48 | private void InstantiateAllSets() 49 | { 50 | var allSets = this.GetType() 51 | .GetProperties() 52 | .Where(pi => pi.PropertyType 53 | .GetCustomAttribute(true) != null) 54 | .Select(pi => new 55 | { 56 | PropertyInfo = pi, 57 | PropertyInstance = this.GetGenericPropertyInstance(pi) 58 | }) 59 | .ToList(); 60 | 61 | allSets.ForEach(a => a.PropertyInfo.SetValue(this, a.PropertyInstance)); 62 | } 63 | 64 | private object GetGenericPropertyInstance(PropertyInfo propertyInfo) 65 | { 66 | var propertyType = propertyInfo.PropertyType; 67 | var implementationType = propertyType; 68 | 69 | if (!propertyType.IsGenericType) 70 | { 71 | throw new InvalidOperationException($"{propertyType} is not a generic type."); 72 | } 73 | 74 | if (propertyType.IsAbstract) 75 | { 76 | implementationType = this.dependencyResolver 77 | .TryGetDependency(propertyType) 78 | ?.GetType() 79 | ?? typeof(DynamoDbSet<>) 80 | .MakeGenericType(propertyType.GetGenericArguments()); 81 | } 82 | 83 | var instance = this.dependencyResolver.TryGetDependency(implementationType); 84 | 85 | if (instance != null) 86 | { 87 | return instance; 88 | } 89 | 90 | var constructor = implementationType 91 | .GetConstructors(BindingFlags.Public | BindingFlags.Instance) 92 | .Single(); 93 | var constructorParams = constructor 94 | .GetParameters() 95 | .Select(pi => pi.ParameterType) 96 | .Select(t => this.dependencyResolver.TryGetDependency(t)) 97 | .Where(dep => dep != null) 98 | .ToList(); 99 | 100 | constructorParams.Add(this.GetType()); 101 | 102 | instance = constructor.Invoke(constructorParams.ToArray()); 103 | 104 | if (instance != null) 105 | { 106 | return instance; 107 | } 108 | 109 | throw new InvalidOperationException( 110 | $"{propertyType.FullName} could not be instantiated."); 111 | } 112 | 113 | private void ListAllTablesByDbSets() 114 | { 115 | var allDbSets = this.GetType() 116 | .GetProperties() 117 | .Where(pi => pi.PropertyType.GetCustomAttribute() != null) 118 | .ToList(); 119 | 120 | foreach (var dbSetInfo in allDbSets) 121 | { 122 | var entityType = dbSetInfo.PropertyType.GetGenericArguments().First(); 123 | 124 | if (this.options.TableNameByEntityTypes.ContainsKey(entityType)) 125 | { 126 | continue; 127 | } 128 | 129 | var tableNameFromAttribute = entityType 130 | .GetCustomAttribute() 131 | ?.TableName; 132 | 133 | this.options.TableNameByEntityTypes[entityType] = tableNameFromAttribute 134 | ?? entityType.Name; 135 | } 136 | } 137 | 138 | private DatabaseFacade GetDatabaseFacadeInstance() 139 | { 140 | var constructor = typeof(DatabaseFacade) 141 | .GetConstructors() 142 | .First(); 143 | var constructorParameters = constructor 144 | .GetParameters() 145 | .Select(pi => this.dependencyResolver.TryGetDependency(pi.ParameterType)) 146 | .Where(i => i != null) 147 | .ToList(); 148 | 149 | constructorParameters.Add(this.GetType()); 150 | 151 | var instance = constructor.Invoke(constructorParameters.ToArray()); 152 | 153 | return instance as DatabaseFacade; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Config/DynamoContextOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using EasyDynamo.Exceptions; 3 | using EasyDynamo.Tests.Fakes; 4 | using System; 5 | using Xunit; 6 | 7 | namespace EasyDynamo.Tests.Config 8 | { 9 | public class DynamoContextOptionsTests 10 | { 11 | private readonly DynamoContextOptionsFake options; 12 | 13 | public DynamoContextOptionsTests() 14 | { 15 | this.options = new DynamoContextOptionsFake(typeof(FakeDynamoContext)) 16 | { 17 | ServiceUrl = "http://localhost:8000", 18 | AccessKeyId = Guid.NewGuid().ToString(), 19 | SecretAccessKey = Guid.NewGuid().ToString(), 20 | Profile = "SomeProfileName", 21 | RegionEndpoint = RegionEndpoint.APNortheast1, 22 | LocalMode = false 23 | }; 24 | } 25 | 26 | [Fact] 27 | public void ValidateLocalMode_NotLocalMode_DoesNotThrowException() 28 | { 29 | var thrown = false; 30 | this.options.LocalMode = false; 31 | 32 | try 33 | { 34 | this.options.ValidateLocalModeFromBase(); 35 | } 36 | catch 37 | { 38 | thrown = true; 39 | } 40 | 41 | Assert.False(thrown); 42 | } 43 | 44 | [Fact] 45 | public void ValidateLocalMode_AllSet_DoesNotThrowException() 46 | { 47 | var thrown = false; 48 | this.options.LocalMode = true; 49 | 50 | try 51 | { 52 | this.options.ValidateLocalModeFromBase(); 53 | } 54 | catch 55 | { 56 | thrown = true; 57 | } 58 | 59 | Assert.False(thrown); 60 | } 61 | 62 | [Theory] 63 | [InlineData(null)] 64 | [InlineData("")] 65 | [InlineData(" ")] 66 | public void ValidateLocalMode_ServiceUrlIsEmpty_ThrowsException(string empty) 67 | { 68 | this.options.LocalMode = true; 69 | this.options.ServiceUrl = empty; 70 | 71 | Assert.Throws( 72 | () => this.options.ValidateLocalModeFromBase()); 73 | } 74 | 75 | [Theory] 76 | [InlineData(null)] 77 | [InlineData("")] 78 | [InlineData(" ")] 79 | public void ValidateLocalMode_AccessKeyIsEmpty_ThrowsException(string empty) 80 | { 81 | this.options.LocalMode = true; 82 | this.options.AccessKeyId = empty; 83 | 84 | Assert.Throws( 85 | () => this.options.ValidateLocalModeFromBase()); 86 | } 87 | 88 | [Theory] 89 | [InlineData(null)] 90 | [InlineData("")] 91 | [InlineData(" ")] 92 | public void ValidateLocalMode_SecretKeyIsEmpty_ThrowsException(string empty) 93 | { 94 | this.options.LocalMode = true; 95 | this.options.SecretAccessKey = empty; 96 | 97 | Assert.Throws( 98 | () => this.options.ValidateLocalModeFromBase()); 99 | } 100 | 101 | [Fact] 102 | public void ValidateCloudMode_IsLocalMode_DoesNotThrowException() 103 | { 104 | var thrown = false; 105 | this.options.LocalMode = true; 106 | 107 | try 108 | { 109 | this.options.ValidateCloudModeFromBase(); 110 | } 111 | catch 112 | { 113 | thrown = true; 114 | } 115 | 116 | Assert.False(thrown); 117 | } 118 | 119 | [Fact] 120 | public void ValidateCloudMode_AllSet_DoesNotThrowException() 121 | { 122 | var thrown = false; 123 | this.options.LocalMode = false; 124 | 125 | try 126 | { 127 | this.options.ValidateCloudModeFromBase(); 128 | } 129 | catch 130 | { 131 | thrown = true; 132 | } 133 | 134 | Assert.False(thrown); 135 | } 136 | 137 | [Fact] 138 | public void ValidateCloudMode_RegionEndpointIsNull_ThrowsException() 139 | { 140 | this.options.LocalMode = false; 141 | this.options.RegionEndpoint = null; 142 | 143 | Assert.Throws( 144 | () => this.options.ValidateCloudModeFromBase()); 145 | } 146 | 147 | [Theory] 148 | [InlineData(null)] 149 | [InlineData("")] 150 | [InlineData(" ")] 151 | public void ValidateLocalMode_ProfileIsEmpty_ThrowsException(string empty) 152 | { 153 | this.options.LocalMode = false; 154 | this.options.Profile = empty; 155 | 156 | Assert.Throws( 157 | () => this.options.ValidateCloudModeFromBase()); 158 | } 159 | 160 | [Theory] 161 | [InlineData(null)] 162 | [InlineData("")] 163 | [InlineData(" ")] 164 | public void UseTableNAme_TableNameIsEmpty_ThrowsException(string empty) 165 | { 166 | Assert.Throws( 167 | () => this.options.UseTableName(empty)); 168 | } 169 | 170 | [Fact] 171 | public void UseTableNAme_ValidTableName_AddsToDictionary() 172 | { 173 | const string TableName = "Articles"; 174 | this.options.UseTableName(TableName); 175 | 176 | Assert.Single(this.options.TableNameByEntityTypesFromBase); 177 | Assert.True(this.options.TableNameByEntityTypesFromBase.ContainsKey(typeof(FakeEntity))); 178 | Assert.Equal(TableName, this.options.TableNameByEntityTypesFromBase[typeof(FakeEntity)]); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Builders/DynamoContextOptionsBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using EasyDynamo.Abstractions; 3 | using EasyDynamo.Tests.Fakes; 4 | using Moq; 5 | using System; 6 | using Xunit; 7 | 8 | namespace EasyDynamo.Tests.Builders 9 | { 10 | public class DynamoContextOptionsBuilderTests 11 | { 12 | private readonly Mock contextOptionsMock; 13 | private readonly DynamoContextOptionsBuilderFake builder; 14 | 15 | public DynamoContextOptionsBuilderTests() 16 | { 17 | this.contextOptionsMock = new Mock(); 18 | this.builder = new DynamoContextOptionsBuilderFake(contextOptionsMock.Object); 19 | } 20 | 21 | [Theory] 22 | [InlineData(null)] 23 | [InlineData("")] 24 | [InlineData(" ")] 25 | public void UseTableName_EmptyTableNamePassed_ThrowsException(string emptyTableName) 26 | { 27 | Assert.Throws( 28 | () => this.builder.UseTableName(emptyTableName)); 29 | } 30 | 31 | [Fact] 32 | public void UseTableName_ValidTableNamePassed_CallsContextOptionsUseTableNameWithCorrectArgument() 33 | { 34 | const string TableName = "FakeEntity_dev"; 35 | this.builder.UseTableName(TableName); 36 | 37 | this.contextOptionsMock 38 | .Verify(co => co.UseTableName(TableName)); 39 | } 40 | 41 | [Fact] 42 | public void UseTableName_ReturnsSameBuilderInstance() 43 | { 44 | var returnedBuilder = this.builder.UseTableName("some-table"); 45 | 46 | Assert.Equal(this.builder, returnedBuilder); 47 | } 48 | 49 | [Theory] 50 | [InlineData(null)] 51 | [InlineData("")] 52 | [InlineData(" ")] 53 | public void UseAccessKeyId_EmptyKeyIdPassed_ThrowsException(string emptyKey) 54 | { 55 | Assert.Throws( 56 | () => this.builder.UseSecretAccessKey(emptyKey)); 57 | } 58 | 59 | [Fact] 60 | public void UseAccessKeyId_ValidKeyPassed_SetContextOptionsAccessKeyIdWithCorrectValue() 61 | { 62 | const string Key = "some-key"; 63 | 64 | this.builder.UseAccessKeyId(Key); 65 | 66 | this.contextOptionsMock 67 | .VerifySet(opt => opt.AccessKeyId = Key); 68 | } 69 | 70 | [Fact] 71 | public void UseAccessKeyId_ReturnsSameBuilderInstance() 72 | { 73 | var returnedBuilder = this.builder.UseAccessKeyId("some-key"); 74 | 75 | Assert.Equal(this.builder, returnedBuilder); 76 | } 77 | 78 | [Theory] 79 | [InlineData(null)] 80 | [InlineData("")] 81 | [InlineData(" ")] 82 | public void UseSecretAccessKey_EmptySecretPassed_ThrowsException(string emptySecret) 83 | { 84 | Assert.Throws( 85 | () => this.builder.UseSecretAccessKey(emptySecret)); 86 | } 87 | 88 | [Fact] 89 | public void UseSecretAccessKey_ValidSecretPassed_SetContextOptionsSecretAccessKeyWithCorrectValue() 90 | { 91 | const string Secret = "some-secret"; 92 | 93 | this.builder.UseSecretAccessKey(Secret); 94 | 95 | this.contextOptionsMock 96 | .VerifySet(opt => opt.SecretAccessKey = Secret); 97 | } 98 | 99 | [Fact] 100 | public void UseSecretAccessKey_ReturnsSameBuilderInstance() 101 | { 102 | var returnedBuilder = this.builder.UseSecretAccessKey("some-secret"); 103 | 104 | Assert.Equal(this.builder, returnedBuilder); 105 | } 106 | 107 | [Theory] 108 | [InlineData(null)] 109 | [InlineData("")] 110 | [InlineData(" ")] 111 | public void UseLocalMode_EmptyUrlPassed_ThrowsException(string emptyServiceUrl) 112 | { 113 | Assert.Throws( 114 | () => this.builder.UseLocalMode(emptyServiceUrl)); 115 | } 116 | 117 | [Fact] 118 | public void UseLocalMode_ValidLocalModePassed_SetContextOptionsServiceUrlWithCorrectValue() 119 | { 120 | const string Url = "http://localhost:8000"; 121 | 122 | this.builder.UseLocalMode(Url); 123 | 124 | this.contextOptionsMock 125 | .VerifySet(opt => opt.ServiceUrl = Url); 126 | } 127 | 128 | [Fact] 129 | public void UseLocalMode_ReturnsSameBuilderInstance() 130 | { 131 | const string Url = "http://localhost:8000"; 132 | var returnedBuilder = this.builder.UseLocalMode(Url); 133 | 134 | Assert.Equal(this.builder, returnedBuilder); 135 | } 136 | 137 | [Theory] 138 | [InlineData(null)] 139 | [InlineData("")] 140 | [InlineData(" ")] 141 | public void UseServiceUrl_EmptyUrlPassed_ThrowsException(string emptyServiceUrl) 142 | { 143 | Assert.Throws( 144 | () => this.builder.UseServiceUrl(emptyServiceUrl)); 145 | } 146 | 147 | [Fact] 148 | public void UseServiceUrl_ValidLocalModePassed_SetContextOptionsServiceUrlWithCorrectValue() 149 | { 150 | const string Url = "http://localhost:8000"; 151 | 152 | this.builder.UseLocalMode(Url); 153 | 154 | this.contextOptionsMock 155 | .VerifySet(opt => opt.ServiceUrl = Url); 156 | } 157 | 158 | [Fact] 159 | public void UseServiceUrl_ReturnsSameBuilderInstance() 160 | { 161 | const string Url = "http://localhost:8000"; 162 | var returnedBuilder = this.builder.UseServiceUrl(Url); 163 | 164 | Assert.Equal(this.builder, returnedBuilder); 165 | } 166 | 167 | [Fact] 168 | public void UseRegionEndpoint_NullRegionPassed_ThrowsException() 169 | { 170 | Assert.Throws( 171 | () => this.builder.UseRegionEndpoint(null)); 172 | } 173 | 174 | [Fact] 175 | public void UseRegionEndpoint_ValidRegionPassed_SetContextOptionsRegionwithCorrectValue() 176 | { 177 | var region = RegionEndpoint.APNortheast1; 178 | 179 | this.builder.UseRegionEndpoint(region); 180 | 181 | this.contextOptionsMock 182 | .VerifySet(co => co.RegionEndpoint = region); 183 | } 184 | 185 | [Fact] 186 | public void UseRegionEndpoint_ReturnsSameBuilderInstance() 187 | { 188 | var returnedBuilder = this.builder.UseRegionEndpoint(RegionEndpoint.APNortheast1); 189 | 190 | Assert.Equal(this.builder, returnedBuilder); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Factories/IndexFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using EasyDynamo.Config; 3 | using EasyDynamo.Factories; 4 | using EasyDynamo.Tests.Fakes; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace EasyDynamo.Tests.Factories 11 | { 12 | public class IndexFactoryTests 13 | { 14 | private readonly IndexFactory factory; 15 | 16 | public IndexFactoryTests() 17 | { 18 | this.factory = new IndexFactory(); 19 | } 20 | 21 | [Fact] 22 | public void CreateRequestIndexes_NullPassed_ReturnsEmptyCollection() 23 | { 24 | var requestIndexes = this.factory.CreateRequestIndexes(null); 25 | 26 | Assert.Empty(requestIndexes); 27 | } 28 | 29 | [Fact] 30 | public void CreateRequestIndexes_EmptyCollectionPassed_ReturnsEmptyCollection() 31 | { 32 | var requestIndexes = this.factory.CreateRequestIndexes(null); 33 | 34 | Assert.Empty(requestIndexes); 35 | } 36 | 37 | [Fact] 38 | public void CreateRequestIndexes_OneConfig_ReturnsSingleRequestIndex() 39 | { 40 | var gsiConfig = new GlobalSecondaryIndexConfiguration 41 | { 42 | HashKeyMemberName = nameof(FakeEntity.Title), 43 | HashKeyMemberType = typeof(string), 44 | IndexName = "GSI_Entities_Title_LastModified", 45 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 46 | RangeKeyMemberType = typeof(DateTime), 47 | ReadCapacityUnits = 3, 48 | WriteCapacityUnits = 5 49 | }; 50 | var gsiConfigs = new List { gsiConfig }; 51 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 52 | 53 | Assert.Single(requestIndexes); 54 | } 55 | 56 | [Fact] 57 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithCorrectIndexName() 58 | { 59 | var gsiConfig = new GlobalSecondaryIndexConfiguration 60 | { 61 | HashKeyMemberName = nameof(FakeEntity.Title), 62 | HashKeyMemberType = typeof(string), 63 | IndexName = "GSI_Entities_Title_LastModified", 64 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 65 | RangeKeyMemberType = typeof(DateTime), 66 | ReadCapacityUnits = 3, 67 | WriteCapacityUnits = 5 68 | }; 69 | var gsiConfigs = new List { gsiConfig }; 70 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 71 | var resultIndex = requestIndexes.Single(); 72 | 73 | Assert.Equal(gsiConfig.IndexName, resultIndex.IndexName); 74 | } 75 | 76 | [Fact] 77 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithProjectionTypeAll() 78 | { 79 | var gsiConfig = new GlobalSecondaryIndexConfiguration 80 | { 81 | HashKeyMemberName = nameof(FakeEntity.Title), 82 | HashKeyMemberType = typeof(string), 83 | IndexName = "GSI_Entities_Title_LastModified", 84 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 85 | RangeKeyMemberType = typeof(DateTime), 86 | ReadCapacityUnits = 3, 87 | WriteCapacityUnits = 5 88 | }; 89 | var gsiConfigs = new List { gsiConfig }; 90 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 91 | var resultIndex = requestIndexes.Single(); 92 | 93 | Assert.Equal(ProjectionType.ALL, resultIndex.Projection.ProjectionType); 94 | } 95 | 96 | [Fact] 97 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithCorrectReadCapacityUnits() 98 | { 99 | var gsiConfig = new GlobalSecondaryIndexConfiguration 100 | { 101 | HashKeyMemberName = nameof(FakeEntity.Title), 102 | HashKeyMemberType = typeof(string), 103 | IndexName = "GSI_Entities_Title_LastModified", 104 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 105 | RangeKeyMemberType = typeof(DateTime), 106 | ReadCapacityUnits = 3, 107 | WriteCapacityUnits = 5 108 | }; 109 | var gsiConfigs = new List { gsiConfig }; 110 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 111 | var resultIndex = requestIndexes.Single(); 112 | 113 | Assert.Equal( 114 | gsiConfig.ReadCapacityUnits, 115 | resultIndex.ProvisionedThroughput.ReadCapacityUnits); 116 | } 117 | 118 | [Fact] 119 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithCorrectWriteCapacityUnits() 120 | { 121 | var gsiConfig = new GlobalSecondaryIndexConfiguration 122 | { 123 | HashKeyMemberName = nameof(FakeEntity.Title), 124 | HashKeyMemberType = typeof(string), 125 | IndexName = "GSI_Entities_Title_LastModified", 126 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 127 | RangeKeyMemberType = typeof(DateTime), 128 | ReadCapacityUnits = 3, 129 | WriteCapacityUnits = 5 130 | }; 131 | var gsiConfigs = new List { gsiConfig }; 132 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 133 | var resultIndex = requestIndexes.Single(); 134 | 135 | Assert.Equal( 136 | gsiConfig.WriteCapacityUnits, 137 | resultIndex.ProvisionedThroughput.WriteCapacityUnits); 138 | } 139 | 140 | [Fact] 141 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithHashKeySchemaElement() 142 | { 143 | var gsiConfig = new GlobalSecondaryIndexConfiguration 144 | { 145 | HashKeyMemberName = nameof(FakeEntity.Title), 146 | HashKeyMemberType = typeof(string), 147 | IndexName = "GSI_Entities_Title_LastModified", 148 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 149 | RangeKeyMemberType = typeof(DateTime), 150 | ReadCapacityUnits = 3, 151 | WriteCapacityUnits = 5 152 | }; 153 | var gsiConfigs = new List { gsiConfig }; 154 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 155 | var resultIndex = requestIndexes.Single(); 156 | 157 | Assert.Contains(resultIndex.KeySchema, ks => 158 | ks.AttributeName == gsiConfig.HashKeyMemberName && 159 | ks.KeyType == KeyType.HASH); 160 | } 161 | 162 | [Fact] 163 | public void CreateRequestIndexes_OneConfig_ReturnsRequestIndexWithRangeKeySchemaElement() 164 | { 165 | var gsiConfig = new GlobalSecondaryIndexConfiguration 166 | { 167 | HashKeyMemberName = nameof(FakeEntity.Title), 168 | HashKeyMemberType = typeof(string), 169 | IndexName = "GSI_Entities_Title_LastModified", 170 | RangeKeyMemberName = nameof(FakeEntity.LastModified), 171 | RangeKeyMemberType = typeof(DateTime), 172 | ReadCapacityUnits = 3, 173 | WriteCapacityUnits = 5 174 | }; 175 | var gsiConfigs = new List { gsiConfig }; 176 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 177 | var resultIndex = requestIndexes.Single(); 178 | 179 | Assert.Contains(resultIndex.KeySchema, ks => 180 | ks.AttributeName == gsiConfig.RangeKeyMemberName && 181 | ks.KeyType == KeyType.RANGE); 182 | } 183 | 184 | [Theory] 185 | [InlineData(3)] 186 | [InlineData(9)] 187 | [InlineData(27)] 188 | [InlineData(81)] 189 | public void CreateRequestIndexes_ManyConfigs_ReturnsAsManyRequestIndexesAsConfigurationsPassed(int expected) 190 | { 191 | var gsiConfigs = Enumerable.Range(1, expected) 192 | .Select(i => new GlobalSecondaryIndexConfiguration 193 | { 194 | HashKeyMemberName = $"Hash-{i}", 195 | HashKeyMemberType = typeof(string), 196 | IndexName = $"GSI_{i}", 197 | RangeKeyMemberName = $"Range-{i}", 198 | RangeKeyMemberType = typeof(int), 199 | ReadCapacityUnits = 3, 200 | WriteCapacityUnits = 5 201 | }); 202 | var requestIndexes = this.factory.CreateRequestIndexes(gsiConfigs); 203 | 204 | Assert.Equal(expected, requestIndexes.Count()); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/EasyDynamo/Extensions/DependencyInjection/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.Extensions.NETCore.Setup; 3 | using Amazon.Runtime.CredentialManagement; 4 | using EasyDynamo.Abstractions; 5 | using EasyDynamo.Attributes; 6 | using EasyDynamo.Builders; 7 | using EasyDynamo.Config; 8 | using EasyDynamo.Core; 9 | using EasyDynamo.Exceptions; 10 | using EasyDynamo.Tools; 11 | using EasyDynamo.Tools.Resolvers; 12 | using EasyDynamo.Tools.Validators; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using System.Reflection; 19 | 20 | namespace EasyDynamo.Extensions.DependencyInjection 21 | { 22 | public static class ServiceCollectionExtensions 23 | { 24 | private static HashSet contextTypesAdded = new HashSet(); 25 | 26 | /// 27 | /// Adds your database context class to the service provider. 28 | /// Use to configure the DynamoContext options. 29 | /// 30 | /// A custom type that implements EasyDynamo.Core.DynamoContext 31 | /// The service collection. 32 | /// The application configuration. 33 | /// Action over EasyDynamo.Config.DynamoContextOptions. 34 | public static IServiceCollection AddDynamoContext( 35 | this IServiceCollection services, 36 | IConfiguration configuration, 37 | Action optionsExpression) 38 | where TContext : DynamoContext 39 | { 40 | InputValidator.ThrowIfNull(optionsExpression); 41 | 42 | var contextOptions = new DynamoContextOptions(typeof(TContext)); 43 | 44 | optionsExpression(contextOptions); 45 | 46 | services.AddSingleton(sp => contextOptions); 47 | 48 | services.AddDynamoContext(configuration); 49 | 50 | return services; 51 | } 52 | 53 | /// 54 | /// Adds your database context class to the service provider. 55 | /// 56 | /// 57 | /// A custom type that implements EasyDynamo.Core.DynamoContext. 58 | /// 59 | /// The service collection. 60 | /// The application configuration. 61 | public static IServiceCollection AddDynamoContext( 62 | this IServiceCollection services, IConfiguration configuration) 63 | where TContext : DynamoContext 64 | { 65 | services.EnsureContextNotAdded(); 66 | 67 | services.AddSingleton(); 68 | services.AddCoreServices(); 69 | 70 | var awsOptions = configuration.GetAWSOptions(); 71 | var contextOptions = services 72 | .BuildServiceProvider() 73 | .GetRequiredService() 74 | .TryGetContextOptions(); 75 | 76 | if (contextOptions == null) 77 | { 78 | contextOptions = new DynamoContextOptions(typeof(TContext)); 79 | 80 | services.AddSingleton(sp => contextOptions); 81 | } 82 | 83 | services.AddDefaultAWSOptions(awsOptions); 84 | 85 | var contextInstance = Instantiator.GetConstructorlessInstance(); 86 | 87 | services.BuildModels(contextInstance, configuration, contextOptions); 88 | 89 | BuildConfiguration(contextInstance, configuration, contextOptions); 90 | 91 | services.AddSingleton(); 92 | services.AddSingleton(typeof(IDynamoDbSet<>), typeof(DynamoDbSet<>)); 93 | 94 | services.AddDynamoClient(awsOptions, contextOptions); 95 | 96 | contextTypesAdded.Add(typeof(TContext)); 97 | 98 | return services; 99 | } 100 | 101 | private static IServiceCollection AddDynamoClient( 102 | this IServiceCollection services, 103 | AWSOptions awsOptions, 104 | IDynamoContextOptions options) 105 | { 106 | var awsCredentials = awsOptions?.Credentials?.GetCredentials(); 107 | 108 | options.Profile = options.Profile ?? awsOptions?.Profile; 109 | options.AccessKeyId = options.AccessKeyId ?? awsCredentials?.AccessKey; 110 | options.SecretAccessKey = options.SecretAccessKey ?? awsCredentials?.SecretKey; 111 | 112 | if (options.LocalMode) 113 | { 114 | AddDynamoLocalClient(services, options); 115 | 116 | return services; ; 117 | } 118 | 119 | options.RegionEndpoint = options.RegionEndpoint ?? awsOptions?.Region; 120 | 121 | AddDynamoCloudClient(services, options, awsOptions); 122 | 123 | return services; 124 | } 125 | 126 | private static void AddDynamoCloudClient( 127 | IServiceCollection services, 128 | IDynamoContextOptions contextOptions, 129 | AWSOptions awsOptions) 130 | { 131 | awsOptions.Profile = awsOptions?.Profile ?? contextOptions.Profile; 132 | awsOptions.Region = awsOptions?.Region ?? contextOptions.RegionEndpoint; 133 | 134 | contextOptions.AwsOptions = awsOptions; 135 | 136 | //contextOptions.ValidateCloudMode(); 137 | 138 | services.AddAWSService(awsOptions); 139 | } 140 | 141 | private static void AddDynamoLocalClient( 142 | IServiceCollection services, 143 | IDynamoContextOptions options) 144 | { 145 | options.AwsOptions.Credentials = AWSCredentialsFactory.GetAWSCredentials( 146 | new CredentialProfile(options.Profile, new CredentialProfileOptions 147 | { 148 | AccessKey = options.AccessKeyId, 149 | SecretKey = options.SecretAccessKey 150 | }), 151 | new CredentialProfileStoreChain()); 152 | 153 | options.AwsOptions.DefaultClientConfig.ServiceURL = options.ServiceUrl; 154 | 155 | //options.ValidateLocalMode(); 156 | 157 | services.AddAWSService(options.AwsOptions); 158 | } 159 | 160 | private static void BuildConfiguration( 161 | TContext contextInstance, 162 | IConfiguration configuration, 163 | IDynamoContextOptions contextOptions) 164 | where TContext : DynamoContext 165 | { 166 | var dynamoOptionsBuilder = new DynamoContextOptionsBuilder(contextOptions); 167 | var configuringMethod = typeof(TContext) 168 | .GetMethod("OnConfiguring", BindingFlags.Instance | BindingFlags.NonPublic); 169 | 170 | configuringMethod.Invoke( 171 | contextInstance, new object[] { dynamoOptionsBuilder, configuration }); 172 | } 173 | 174 | private static void BuildModels( 175 | this IServiceCollection services, 176 | TContext contextInstance, 177 | IConfiguration configuration, 178 | IDynamoContextOptions contextOptions) 179 | where TContext : DynamoContext 180 | { 181 | var modelBuilder = new ModelBuilder(contextOptions); 182 | var modelCreatingMethod = typeof(TContext) 183 | .GetMethod("OnModelCreating", BindingFlags.Instance | BindingFlags.NonPublic); 184 | 185 | modelCreatingMethod.Invoke(contextInstance, new object[] { modelBuilder, configuration }); 186 | 187 | modelBuilder.EntityConfigurations 188 | .Values 189 | .ToList() 190 | .ForEach(config => services.AddSingleton(config)); 191 | } 192 | 193 | private static IServiceCollection AddCoreServices(this IServiceCollection services) 194 | { 195 | typeof(ServiceCollectionExtensions) 196 | .Assembly 197 | .GetTypes() 198 | .Where(t => t.IsClass && 199 | !t.IsAbstract && 200 | !t.IsGenericType && 201 | t.GetCustomAttribute() == null && 202 | t.GetInterfaces() 203 | .Any(i => i.Name == $"I{t.Name}")) 204 | .Select(t => new 205 | { 206 | Interface = t.GetInterface($"I{t.Name}"), 207 | Implementation = t 208 | }) 209 | .ToList() 210 | .ForEach(s => services.AddTransient(s.Interface, s.Implementation)); 211 | 212 | return services; 213 | } 214 | 215 | private static IServiceCollection EnsureContextNotAdded( 216 | this IServiceCollection services) 217 | { 218 | var isAlreadyAddedInServices = services 219 | .Any(s => s.ImplementationType == typeof(TContext)); 220 | 221 | if (contextTypesAdded.Contains(typeof(TContext)) || isAlreadyAddedInServices) 222 | { 223 | throw new DynamoContextConfigurationException( 224 | $"You can invoke AddDynamoContext<{typeof(TContext).FullName}> only once."); 225 | } 226 | 227 | return services; 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/EasyDynamo/Builders/EntityTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | using EasyDynamo.Abstractions; 2 | using EasyDynamo.Config; 3 | using EasyDynamo.Core; 4 | using EasyDynamo.Extensions; 5 | using EasyDynamo.Tools.Validators; 6 | using System; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | 10 | namespace EasyDynamo.Builders 11 | { 12 | public class EntityTypeBuilder : IEntityTypeBuilder 13 | where TContext : DynamoContext 14 | where TEntity : class, new() 15 | { 16 | private readonly EntityConfiguration entityConfig; 17 | private readonly IDynamoContextOptions contextOptions; 18 | 19 | protected internal EntityTypeBuilder( 20 | EntityConfiguration entityConfig, 21 | IDynamoContextOptions contextOptions) 22 | { 23 | this.entityConfig = entityConfig; 24 | this.contextOptions = contextOptions; 25 | } 26 | 27 | /// 28 | /// Adds a table name for a specific entity type. 29 | /// 30 | /// 31 | public IEntityTypeBuilder HasTable(string tableName) 32 | { 33 | InputValidator.ThrowIfNullOrWhitespace( 34 | tableName, 35 | $"Parameter cannot be empty: {nameof(tableName)}."); 36 | 37 | this.entityConfig.TableName = tableName; 38 | this.contextOptions.UseTableName(tableName); 39 | 40 | return this; 41 | } 42 | 43 | /// 44 | /// Adds info for a Global Secondary Index to the configuration. 45 | /// 46 | /// 47 | public IEntityTypeBuilder HasGlobalSecondaryIndex( 48 | string indexName, 49 | Expression> hashKeyMemberExpression, 50 | Expression> rangeKeyMemberExpression, 51 | long readCapacityUnits = 1, 52 | long writeCapacityUnits = 1) 53 | { 54 | InputValidator.ThrowIfAnyNullOrWhitespace( 55 | indexName, hashKeyMemberExpression, rangeKeyMemberExpression); 56 | 57 | var indexConfig = this.entityConfig 58 | .Indexes 59 | .FirstOrDefault(i => i.IndexName == indexName) 60 | ?? new GlobalSecondaryIndexConfiguration(); 61 | indexConfig.IndexName = indexName; 62 | indexConfig.HashKeyMemberName = hashKeyMemberExpression.TryGetMemberName(); 63 | indexConfig.HashKeyMemberType = hashKeyMemberExpression.TryGetMemberType(); 64 | indexConfig.RangeKeyMemberName = rangeKeyMemberExpression.TryGetMemberName(); 65 | indexConfig.RangeKeyMemberType = rangeKeyMemberExpression.TryGetMemberType(); 66 | indexConfig.ReadCapacityUnits = readCapacityUnits; 67 | indexConfig.WriteCapacityUnits = writeCapacityUnits; 68 | 69 | this.entityConfig.Indexes.Add(indexConfig); 70 | 71 | return this; 72 | } 73 | 74 | /// 75 | /// Adds info for a Global Secondary Index to the configuration. 76 | /// 77 | /// 78 | public IEntityTypeBuilder HasGlobalSecondaryIndex( 79 | Action indexAction) 80 | { 81 | InputValidator.ThrowIfNull(indexAction, "indexAction cannot be null."); 82 | 83 | var newIndexConfig = new GlobalSecondaryIndexConfiguration(); 84 | 85 | indexAction(newIndexConfig); 86 | 87 | InputValidator.ThrowIfAnyNullOrWhitespace( 88 | newIndexConfig.HashKeyMemberName, 89 | newIndexConfig.IndexName); 90 | 91 | if (newIndexConfig.HashKeyMemberType == null) 92 | { 93 | newIndexConfig.HashKeyMemberType = typeof(TEntity) 94 | .GetProperty(newIndexConfig.HashKeyMemberName) 95 | .PropertyType; 96 | } 97 | 98 | if (!string.IsNullOrWhiteSpace(newIndexConfig.RangeKeyMemberName) && 99 | newIndexConfig.RangeKeyMemberType == null) 100 | { 101 | newIndexConfig.RangeKeyMemberType = typeof(TEntity) 102 | .GetProperty(newIndexConfig.RangeKeyMemberName) 103 | .PropertyType; 104 | } 105 | 106 | var existingIndexConfig = this.entityConfig 107 | .Indexes 108 | .FirstOrDefault(i => i.IndexName == newIndexConfig.IndexName); 109 | 110 | if (existingIndexConfig != null) 111 | { 112 | indexAction(existingIndexConfig); 113 | 114 | return this; 115 | } 116 | 117 | this.entityConfig.Indexes.Add(newIndexConfig); 118 | 119 | return this; 120 | } 121 | 122 | /// 123 | /// Specify the primary key for that entity type. 124 | /// 125 | public IEntityTypeBuilder HasPrimaryKey( 126 | Expression> keyExpression) 127 | { 128 | InputValidator.ThrowIfNull(keyExpression, "keyExpression cannot be null."); 129 | 130 | this.entityConfig.HashKeyMemberExpression = keyExpression; 131 | this.entityConfig.HashKeyMemberName = keyExpression.TryGetMemberName(); 132 | this.entityConfig.HashKeyMemberType = keyExpression.TryGetMemberType(); 133 | 134 | return this; 135 | } 136 | 137 | /// 138 | /// Ignore that property when save the entity to the database. 139 | /// All ignored members will be set to its default value before saving in the database. 140 | /// 141 | public IEntityTypeBuilder Ignore( 142 | Expression> propertyExpression) 143 | { 144 | InputValidator.ThrowIfNull(propertyExpression); 145 | 146 | var memberName = propertyExpression.TryGetMemberName(); 147 | 148 | this.entityConfig.IgnoredMembersNames.Add(memberName); 149 | this.entityConfig.IgnoredMembersExpressions.Add(propertyExpression); 150 | 151 | return this; 152 | } 153 | 154 | /// 155 | /// Ignore that property when save the entity to the database. 156 | /// All ignored members will be set to its default value before saving in the database. 157 | /// 158 | /// 159 | public IEntityTypeBuilder Ignore(string propertyName) 160 | { 161 | InputValidator.ThrowIfNullOrWhitespace(propertyName); 162 | 163 | this.entityConfig.IgnoredMembersNames.Add(propertyName); 164 | 165 | return this; 166 | } 167 | 168 | /// 169 | /// Specifies either the entity should be validated against its 170 | /// configuration and its attributes before saving in the database ot not. 171 | /// True by default. 172 | /// 173 | public IEntityTypeBuilder ValidateOnSave(bool validate = true) 174 | { 175 | this.entityConfig.ValidateOnSave = validate; 176 | 177 | return this; 178 | } 179 | 180 | /// 181 | /// Returns a property builder for that entity member. 182 | /// 183 | public IPropertyTypeBuilder Property( 184 | Expression> propertyExpression) 185 | { 186 | InputValidator.ThrowIfNull(propertyExpression); 187 | 188 | var memberName = propertyExpression.TryGetMemberName(); 189 | var propertyConfig = this.entityConfig 190 | .Properties 191 | .SingleOrDefault(p => p.MemberName == memberName); 192 | 193 | if (propertyConfig == null) 194 | { 195 | propertyConfig = new PropertyConfiguration(memberName); 196 | 197 | this.entityConfig.Properties.Add(propertyConfig); 198 | } 199 | 200 | return new PropertyTypeBuilder(propertyConfig); 201 | } 202 | 203 | /// 204 | /// Specify the ReadCapacityUnits for the entity's corresponding dynamo table. 205 | /// 206 | /// 207 | public IEntityTypeBuilder HasReadCapacityUnits(long readCapacityUnits) 208 | { 209 | InputValidator.ThrowIfNotPositive( 210 | readCapacityUnits, 211 | "ReadCapacityUnits should be a positive integer."); 212 | 213 | this.entityConfig.ReadCapacityUnits = readCapacityUnits; 214 | 215 | return this; 216 | } 217 | 218 | /// 219 | /// Specify the WriteCapacityUnits for the entity's corresponding dynamo table. 220 | /// 221 | /// 222 | public IEntityTypeBuilder HasWriteCapacityUnits(long writeCapacityUnits) 223 | { 224 | InputValidator.ThrowIfNotPositive( 225 | writeCapacityUnits, 226 | "WriteCapacityUnits should be a positive integer."); 227 | 228 | this.entityConfig.WriteCapacityUnits = writeCapacityUnits; 229 | 230 | return this; 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/EasyDynamo.Tests/Extensions/DependencyInjection/ServiceCollectionExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon; 2 | using Amazon.DynamoDBv2; 3 | using Amazon.DynamoDBv2.DataModel; 4 | using Amazon.Extensions.NETCore.Setup; 5 | using EasyDynamo.Abstractions; 6 | using EasyDynamo.Config; 7 | using EasyDynamo.Exceptions; 8 | using EasyDynamo.Extensions.DependencyInjection; 9 | using EasyDynamo.Tests.Fakes; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Moq; 13 | using System; 14 | using System.Threading.Tasks; 15 | using Xunit; 16 | 17 | namespace EasyDynamo.Tests.Extensions.DependencyInjection 18 | { 19 | public class ServiceCollectionExtensionsTests 20 | { 21 | private readonly Mock contextOptionsMock; 22 | private readonly Mock configurationMock; 23 | private readonly IServiceCollection services; 24 | 25 | public ServiceCollectionExtensionsTests() 26 | { 27 | this.contextOptionsMock = new Mock(); 28 | this.configurationMock = new Mock(); 29 | this.services = new ServiceCollection(); 30 | } 31 | 32 | #region AddDynamoContext with only IConfiguration 33 | 34 | [Fact] 35 | public async Task AddDynamoContext_AddsAwsOptionsToServiceProvider() 36 | { 37 | this.contextOptionsMock 38 | .SetupGet(o => o.Profile) 39 | .Returns("ApplicationDevelopment"); 40 | this.contextOptionsMock 41 | .SetupGet(o => o.LocalMode) 42 | .Returns(false); 43 | this.contextOptionsMock 44 | .SetupGet(o => o.RegionEndpoint) 45 | .Returns(RegionEndpoint.APNortheast1); 46 | 47 | await TestRetrier.RetryAsync(() => 48 | { 49 | services.AddDynamoContext(this.configurationMock.Object); 50 | 51 | var serviceProvider = this.services.BuildServiceProvider(); 52 | var awsOptions = serviceProvider.GetService(); 53 | 54 | Assert.NotNull(awsOptions); 55 | }); 56 | } 57 | 58 | [Fact] 59 | public async Task AddDynamoContext_InvokesOnConfiguring() 60 | { 61 | this.contextOptionsMock 62 | .SetupGet(o => o.Profile) 63 | .Returns("ApplicationDevelopment"); 64 | this.contextOptionsMock 65 | .SetupGet(o => o.LocalMode) 66 | .Returns(false); 67 | this.contextOptionsMock 68 | .SetupGet(o => o.RegionEndpoint) 69 | .Returns(RegionEndpoint.APNortheast1); 70 | 71 | await TestRetrier.RetryAsync(() => 72 | { 73 | FakeDynamoContext.OnConfiguringInvoked = false; 74 | 75 | services.AddDynamoContext(this.configurationMock.Object); 76 | 77 | Assert.True(FakeDynamoContext.OnConfiguringInvoked); 78 | }); 79 | } 80 | 81 | [Fact] 82 | public async Task AddDynamoContext_InvokesOnModelCreating() 83 | { 84 | this.contextOptionsMock 85 | .SetupGet(o => o.Profile) 86 | .Returns("ApplicationDevelopment"); 87 | this.contextOptionsMock 88 | .SetupGet(o => o.LocalMode) 89 | .Returns(false); 90 | this.contextOptionsMock 91 | .SetupGet(o => o.RegionEndpoint) 92 | .Returns(RegionEndpoint.APNortheast1); 93 | 94 | await TestRetrier.RetryAsync(() => 95 | { 96 | FakeDynamoContext.OnModelCreatingInvoked = false; 97 | 98 | services.AddDynamoContext(this.configurationMock.Object); 99 | 100 | Assert.True(FakeDynamoContext.OnModelCreatingInvoked); 101 | }); 102 | } 103 | 104 | [Fact] 105 | public async Task AddDynamoContext_AddsContextToServicesAsSingleton() 106 | { 107 | this.contextOptionsMock 108 | .SetupGet(o => o.Profile) 109 | .Returns("ApplicationDevelopment"); 110 | this.contextOptionsMock 111 | .SetupGet(o => o.LocalMode) 112 | .Returns(false); 113 | this.contextOptionsMock 114 | .SetupGet(o => o.RegionEndpoint) 115 | .Returns(RegionEndpoint.APNortheast1); 116 | 117 | await TestRetrier.RetryAsync(() => 118 | { 119 | services.AddDynamoContext(this.configurationMock.Object); 120 | 121 | Assert.Contains(this.services, s => 122 | s.ImplementationType == typeof(FakeDynamoContext) && 123 | s.Lifetime == ServiceLifetime.Singleton); 124 | }); 125 | } 126 | 127 | [Fact] 128 | public async Task AddDynamoContext_AddsDynamoContextToServicesAsSingleton() 129 | { 130 | this.contextOptionsMock 131 | .SetupGet(o => o.Profile) 132 | .Returns("ApplicationDevelopment"); 133 | this.contextOptionsMock 134 | .SetupGet(o => o.LocalMode) 135 | .Returns(false); 136 | this.contextOptionsMock 137 | .SetupGet(o => o.RegionEndpoint) 138 | .Returns(RegionEndpoint.APNortheast1); 139 | 140 | await TestRetrier.RetryAsync(() => 141 | { 142 | services.AddDynamoContext(this.configurationMock.Object); 143 | 144 | Assert.Contains(this.services, s => 145 | s.ServiceType == typeof(IDynamoDBContext) && 146 | s.ImplementationType == typeof(DynamoDBContext) && 147 | s.Lifetime == ServiceLifetime.Singleton); 148 | }); 149 | } 150 | 151 | [Fact] 152 | public async Task AddDynamoContext_CloudMode_AddsDynamoClientToServices() 153 | { 154 | this.contextOptionsMock 155 | .SetupGet(o => o.Profile) 156 | .Returns("ApplicationDevelopment"); 157 | this.contextOptionsMock 158 | .SetupGet(o => o.LocalMode) 159 | .Returns(false); 160 | this.contextOptionsMock 161 | .SetupGet(o => o.RegionEndpoint) 162 | .Returns(RegionEndpoint.APNortheast1); 163 | 164 | await TestRetrier.RetryAsync(() => 165 | { 166 | services.AddDynamoContext(this.configurationMock.Object); 167 | 168 | Assert.Contains(this.services, s => 169 | s.ServiceType == typeof(IAmazonDynamoDB) && 170 | s.ImplementationType == typeof(AmazonDynamoDBClient)); 171 | }); 172 | } 173 | 174 | [Fact] 175 | public async Task AddDynamoContext_LocalMode_AddsDynamoClientToServices() 176 | { 177 | this.contextOptionsMock 178 | .SetupGet(o => o.Profile) 179 | .Returns("ApplicationDevelopment"); 180 | this.contextOptionsMock 181 | .SetupGet(o => o.LocalMode) 182 | .Returns(false); 183 | this.contextOptionsMock 184 | .SetupGet(o => o.RegionEndpoint) 185 | .Returns(RegionEndpoint.APNortheast1); 186 | 187 | await TestRetrier.RetryAsync(() => 188 | { 189 | services.AddDynamoContext(this.configurationMock.Object); 190 | 191 | Assert.Contains(this.services, s => 192 | s.ServiceType == typeof(IAmazonDynamoDB) && 193 | s.ImplementationType == typeof(AmazonDynamoDBClient)); 194 | }); 195 | } 196 | 197 | #endregion 198 | 199 | #region AddDynamoContext with ContextOptions and IConfiguration 200 | 201 | [Fact] 202 | public async Task AddDynamoContext_ValidOptions_AddsAwsOptionsToServiceProvider() 203 | { 204 | await TestRetrier.RetryAsync(() => 205 | { 206 | services.AddDynamoContext( 207 | this.configurationMock.Object, 208 | options => 209 | { 210 | options.Profile = "ApplicationDevelopment"; 211 | options.LocalMode = false; 212 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 213 | }); 214 | 215 | var serviceProvider = this.services.BuildServiceProvider(); 216 | var awsOptions = serviceProvider.GetService(); 217 | 218 | Assert.NotNull(awsOptions); 219 | }); 220 | } 221 | 222 | [Fact] 223 | public async Task AddDynamoContext_ValidOptions_ContextAddedTwice_ThrowsException() 224 | { 225 | await TestRetrier.RetryAsync(() => 226 | { 227 | services.AddDynamoContext( 228 | this.configurationMock.Object, 229 | options => 230 | { 231 | options.Profile = "ApplicationDevelopment"; 232 | options.LocalMode = false; 233 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 234 | }); 235 | 236 | Assert.Throws( 237 | () => services.AddDynamoContext( 238 | this.configurationMock.Object, 239 | options => 240 | { 241 | options.Profile = "ApplicationDevelopment"; 242 | options.LocalMode = false; 243 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 244 | })); 245 | }); 246 | } 247 | 248 | [Fact] 249 | public async Task AddDynamoContext_ValidOptions_InvokesOnConfiguring() 250 | { 251 | await TestRetrier.RetryAsync(() => 252 | { 253 | FakeDynamoContext.OnConfiguringInvoked = false; 254 | 255 | services.AddDynamoContext( 256 | this.configurationMock.Object, 257 | options => 258 | { 259 | options.Profile = "ApplicationDevelopment"; 260 | options.LocalMode = false; 261 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 262 | }); 263 | 264 | services.AddDynamoContext(this.configurationMock.Object); 265 | 266 | Assert.True(FakeDynamoContext.OnConfiguringInvoked); 267 | }); 268 | } 269 | 270 | [Fact] 271 | public async Task AddDynamoContext_ValidOptions_InvokesOnModelCreating() 272 | { 273 | await TestRetrier.RetryAsync(() => 274 | { 275 | FakeDynamoContext.OnModelCreatingInvoked = false; 276 | 277 | services.AddDynamoContext( 278 | this.configurationMock.Object, 279 | options => 280 | { 281 | options.Profile = "ApplicationDevelopment"; 282 | options.LocalMode = false; 283 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 284 | }); 285 | 286 | Assert.True(FakeDynamoContext.OnModelCreatingInvoked); 287 | }); 288 | } 289 | 290 | [Fact] 291 | public async Task AddDynamoContext_ValidOptions_AddsContextToServicesAsSingleton() 292 | { 293 | await TestRetrier.RetryAsync(() => 294 | { 295 | services.AddDynamoContext( 296 | this.configurationMock.Object, 297 | options => 298 | { 299 | options.Profile = "ApplicationDevelopment"; 300 | options.LocalMode = false; 301 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 302 | }); 303 | 304 | Assert.Contains(this.services, s => 305 | s.ImplementationType == typeof(FakeDynamoContext) && 306 | s.Lifetime == ServiceLifetime.Singleton); 307 | }); 308 | } 309 | 310 | [Fact] 311 | public async Task AddDynamoContext_ValidOptions_AddsDynamoContextToServicesAsSingleton() 312 | { 313 | await TestRetrier.RetryAsync(() => 314 | { 315 | services.AddDynamoContext( 316 | this.configurationMock.Object, 317 | options => 318 | { 319 | options.Profile = "ApplicationDevelopment"; 320 | options.LocalMode = false; 321 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 322 | }); 323 | 324 | Assert.Contains(this.services, s => 325 | s.ServiceType == typeof(IDynamoDBContext) && 326 | s.ImplementationType == typeof(DynamoDBContext) && 327 | s.Lifetime == ServiceLifetime.Singleton); 328 | }); 329 | } 330 | 331 | [Fact] 332 | public async Task AddDynamoContext_ValidOptionsAndCloudMode_AddsDynamoClientToServices() 333 | { 334 | await TestRetrier.RetryAsync(() => 335 | { 336 | services.AddDynamoContext( 337 | this.configurationMock.Object, 338 | options => 339 | { 340 | options.Profile = "ApplicationDevelopment"; 341 | options.LocalMode = false; 342 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 343 | }); 344 | 345 | Assert.Contains(this.services, s => 346 | s.ServiceType == typeof(IAmazonDynamoDB) && 347 | s.ImplementationType == typeof(AmazonDynamoDBClient)); 348 | }); 349 | } 350 | 351 | [Fact] 352 | public async Task AddDynamoContext_ValidOptionsAndLocalMode_AddsDynamoClientToServices() 353 | { 354 | await TestRetrier.RetryAsync(() => 355 | { 356 | services.AddDynamoContext( 357 | this.configurationMock.Object, 358 | options => 359 | { 360 | options.Profile = "ApplicationDevelopment"; 361 | options.LocalMode = true; 362 | options.RegionEndpoint = RegionEndpoint.APNortheast1; 363 | }); 364 | 365 | Assert.Contains(this.services, s => 366 | s.ServiceType == typeof(IAmazonDynamoDB) && 367 | s.ImplementationType == typeof(AmazonDynamoDBClient)); 368 | }); 369 | } 370 | 371 | [Fact] 372 | public async Task AddDynamoContext_OptionsIsNull_ThrowsException() 373 | { 374 | await TestRetrier.RetryAsync(() => 375 | { 376 | Assert.Throws( 377 | () => services.AddDynamoContext( 378 | this.configurationMock.Object, 379 | default(Action))); 380 | }); 381 | } 382 | 383 | #endregion 384 | } 385 | } 386 | --------------------------------------------------------------------------------