using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.CompilerServices; using System.Globalization; using static System.Runtime.InteropServices.JavaScript.JSType; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using Microsoft.AspNetCore.Server.IIS.Core; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Google.Protobuf.WellKnownTypes; using ServerCore; using ServerBase; using DYNAMO_DB_TABLE_FULL_NAME = System.String; using DYNAMO_DB_TABLE_NAME = System.String; namespace ServerBase; //============================================================================================= // DynamoDb Document 기반 시간 설정 // // - kangms //============================================================================================= public class DbTimestamp { public bool IsSet { get; set; } = false; public DateTime ProcessedTime { get; set; } = DateTimeHelper.MinTime; public void setProcessedTime(DateTime currentTime) { ProcessedTime = currentTime; IsSet = true; } public void reset() { ProcessedTime = DateTimeHelper.MinTime; IsSet = false; } } //============================================================================================= // DynamoDb Document 기반 Query Context // // - kangms //============================================================================================= public partial class DynamoDbDocumentQueryContext : IQueryContext { private readonly DYNAMO_DB_TABLE_FULL_NAME m_table_full_name; private readonly Document m_document; private QueryType m_query_type; // TransactionCanceledException 관련 설정 private DynamoDbQueryExceptionNotifier.ExceptionHandler? m_exception_handler_nullable; public DynamoDbDocumentQueryContext( DYNAMO_DB_TABLE_FULL_NAME tableFullName , Document document, QueryType queryType ) { m_table_full_name = tableFullName; m_document = document; m_query_type = queryType; } public DynamoDbDocumentQueryContext( DYNAMO_DB_TABLE_FULL_NAME tableFullName , Document document, QueryType queryType , DynamoDbQueryExceptionNotifier.ExceptionHandler? exceptionHandler ) { m_table_full_name = tableFullName; m_document = document; m_query_type = queryType; m_exception_handler_nullable = exceptionHandler; } public bool isValid() { if (true == m_table_full_name.isNullOrWhiteSpace()) { Log.getLogger().error($"TableFullName is Null or WhiteSpace !!! - {toBasicString()}"); return false; } if (null == m_document) { Log.getLogger().error($"DynamoDbDocument is null !!! - {toBasicString()}"); return false; } if (QueryType.None == m_query_type) { Log.getLogger().error($"DynamoDbDocument QueryType invalid !!! - {toBasicString()}"); return false; } return true; } public DYNAMO_DB_TABLE_FULL_NAME getTableFullName() { ConditionValidCheckHelper.throwIfFalseWithCondition(() => false == m_table_full_name.isNullOrWhiteSpace(), () => $"Invalid TableFullName !!!, Null of WhiteSpace !!! - {m_document.toPKSK()}"); return m_table_full_name; } public string toBasicString() { return $"{this.getTypeName()}, TableFullName:{m_table_full_name}, {m_query_type}, {m_document?.toBasicString()}"; } } //============================================================================================= // PK(Partition Key) : Primary Key 내의 Partition Key + 해당 정보의 Domain 식별키 // SK(Sort Key) : Primary Key 내의 Sort Key, 해당 정보의 고유 식별키 // DocType : DynamoDbDocBase 상속 객체명 // Attrib ... : AttribBase 상속 객체명 // CreatedDateTime : Item Document 생성 시간 // UpdateedDateTime : Item Document 마지막 업데이트 시간 (생성, 삭제, 복구시 동일 시간으로 반영) // DeletedDateTime : Item Document 삭제 시간 // RestoredDateTime : Item Document 복구 시간 // - kangms //============================================================================================= public abstract partial class DynamoDbDocBase : IRowData { // 관계된 Table Name, 특화된 테이블명이 있다면 재정의해야 한다. - kangms public virtual DYNAMO_DB_TABLE_NAME TableName { get { return DynamoDbDefine.TableNames.Main; } } public const string CreatedDateTime = "CreatedDateTime"; public const string UpdatedDateTime = "UpdatedDateTime"; public const string DeletedDateTime = "DeletedDateTime"; public const string RestoredDateTime = "RestoredDateTime"; private PrimaryKey m_key = new(); // DocType은 DynamoDb Document 정보를 Read시 식별키로 사용되기 때문에 고유하게 정의되어야 한다 !!! - kangms [JsonProperty("DocType")] private readonly string m_doc_type; public const string ATTRIBUTE_PATH = "attribute_path"; private ConcurrentDictionary m_attrib_wrappers = new(); private DbTimestamp m_created_datetime = new(); private DbTimestamp m_updated_datetime = new(); private DbTimestamp m_deleted_datetime = new(); private DbTimestamp m_restored_datetime = new(); private string m_combination_key_for_pk = string.Empty; private string m_combination_key_for_sk = string.Empty; private Result m_initialize_result = new(); private bool m_is_enable_ttl = false; private Int64 m_ttl_seconds = default; private QueryType m_query_type = QueryType.None; private DynamoDbQueryExceptionNotifier.ExceptionHandler? m_exception_handler_nullable; public DynamoDbDocBase(string docType) { m_doc_type = docType; } // 필독 !!! // 최초 등록시 TTL 설정을 적용해야 한다. // TTL 설정이 적용되면 TTL 시간이 다시 적용됨을 유의 !!! - kangms public DynamoDbDocBase(string docType, Int64 ttlSeconds) { m_doc_type = docType; enableTTL(ttlSeconds); } public void SetTtlSeconds(Int64 ttlSeconds) { m_ttl_seconds = ttlSeconds; } public bool isNotSetupQueryType() { if(QueryType.None == m_query_type) { return true; } return false; } protected bool appendAttribWrapper(IAttribWrapper wrapper) { var result = getInitializeResult(); if (false == m_attrib_wrappers.TryAdd(wrapper.getAttribType(), wrapper)) { var error_code = ServerErrorCode.DynamoDbDocAttribTypeDuplicated; var err_msg = $"Failed to TryAdd AttribWrapper !!!, duplicated AttribType : AttribType:{wrapper.getAttribType()} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(err_msg); return false; } return true; } public virtual async Task newDoc4Query() { var result = getInitializeResult(); if(result.isFail()) { return result; } result = await onPrepareCRUD(); if (result.isFail()) { return result; } var current_time = DateTimeHelper.Current; setCreatedDateTime(current_time); changeUpdatedDateTime(current_time); m_query_type = QueryType.Insert; return result; } public virtual async Task updateDoc4Query() { var result = getInitializeResult(); if (result.isFail()) { return result; } result = await onPrepareCRUD(); if (result.isFail()) { return result; } changeUpdatedDateTime(DateTimeHelper.Current); m_query_type = QueryType.Update; return result; } public virtual async Task upsertDoc4Query() { var result = getInitializeResult(); if (result.isFail()) { return result; } result = await onPrepareCRUD(); if (result.isFail()) { return result; } var current_time = DateTimeHelper.Current; setCreatedDateTime(current_time); changeUpdatedDateTime(current_time); m_query_type = QueryType.Upsert; return result; } public virtual async Task deleteDoc4Query() { var result = getInitializeResult(); if (result.isFail()) { return result; } result = await onPrepareCRUD(); if (result.isFail()) { return result; } setDeletedDateTime(DateTimeHelper.Current); m_query_type = QueryType.Delete; return result; } public virtual async Task onCopyFromDocument(Amazon.DynamoDBv2.DocumentModel.Document fromDocument) { return await Task.Run(() => { var result = new Result(); var err_msg = string.Empty; var doc_type = fromDocument.getDocType(); var pk = fromDocument.getPK(); var sk = fromDocument.getSK(); try { result = getInitializeResult(); if (result.isFail()) { return result; } var error_code = fillUpPrimaryKey(pk, sk); if (error_code.isFail()) { err_msg = $"Failed to fillUpPrimaryKey() !!! : {error_code}"; result.setFail(error_code, err_msg); return result; } var ttl_name = DynamoDbClient.TTL_NAME; if (fromDocument.Contains(ttl_name)) { fromDocument[ttl_name].AsString(); } if (false == fillUpDocType(doc_type)) { err_msg = $"Failed to fillUpDocType() !!! : docType:{doc_type} - PK:{pk}, SK:{sk}"; result.setFail(ServerErrorCode.DynamoDbDocTypeNotMatch, err_msg); return result; } foreach (var each in m_attrib_wrappers) { var attrib_wrapper = each.Value; NullReferenceCheckHelper.throwIfNull(attrib_wrapper, () => $"attrib_wrapper is null !!! - docType:{doc_type}, PK:{pk}, SK:{sk}"); if (false == attrib_wrapper.onCopyFromDynamoDbEntry(fromDocument)) { err_msg = $"Failed to onCopyFromDynamoDbEntry() !!! : attribType:{attrib_wrapper.getAttribType()} - docType:{doc_type}, PK:{pk}, SK:{sk}"; result.setFail(ServerErrorCode.DynamoDbDocAttribWrapperCopyFailed, err_msg); return result; } } if (false == fromDocument.getTimestampByDB_TIMESTAMP(DynamoDbDocBase.CreatedDateTime, out var created_time)) { err_msg = $"Failed to getTimestampByDB_TIMESTAMP() !!!, CreatedDateTime - docType:{doc_type}, PK:{pk}, SK:{sk}"; Log.getLogger().error(err_msg); } else { setCreatedDateTime(created_time); } if (false == fromDocument.getTimestampByDB_TIMESTAMP(DynamoDbDocBase.UpdatedDateTime, out var updated_time)) { err_msg = $"Failed to getTimestampByDB_TIMESTAMP() !!!, UpdatedDateTime - docType:{doc_type}, PK:{pk}, SK:{sk}"; Log.getLogger().error(err_msg); } else { setUpdatedDateTime(updated_time); } if (false == fromDocument.getTimestampByDB_TIMESTAMP(DynamoDbDocBase.DeletedDateTime, out var deleteded_time)) { err_msg = $"Failed to getTimestampByDB_TIMESTAMP() !!!, DeletedDateTime - docType:{doc_type}, PK:{pk}, SK:{sk}"; Log.getLogger().debug(err_msg); } else { setDeletedDateTime(deleteded_time); } if (false == fromDocument.getTimestampByDB_TIMESTAMP(DynamoDbDocBase.RestoredDateTime, out var restored_time)) { err_msg = $"Failed to getTimestampByDB_TIMESTAMP() !!!, RestoredDateTime - docType:{doc_type}, PK:{pk}, SK:{sk}"; Log.getLogger().debug(err_msg); } else { setRestoredDateTime(restored_time); } } catch (Exception e) { err_msg = $"Exception !!!, Failed to perform in DynamoDbDocBase.onCopyFromDocument() !!! : exception:{e} - docType:{doc_type}, PK:{pk}, SK:{sk}"; result.setFail(ServerErrorCode.DynamoDbDocCopyFailedFromDocument, err_msg); Log.getLogger().fatal(result.toBasicString()); return result; } return result; }); } public virtual async Task<(Result, Amazon.DynamoDBv2.DocumentModel.Document)> onCopyToDocument() { return await Task.Run(() => { var toDocument = new Amazon.DynamoDBv2.DocumentModel.Document(); var result = new Result(); var err_msg = string.Empty; var doc_type = getDocType(); var pk = onMakePK(); var sk = onMakeSK(); try { result = getInitializeResult(); if (result.isFail()) { return (result, toDocument); } var error_code = fillUpPrimaryKey(onMakePK(), onMakeSK()); if (error_code.isFail()) { err_msg = $"Failed to fillUpPrimaryKey() !!! : {error_code}"; result.setFail(error_code, err_msg); return (result, toDocument); } if (false == fillUpDocType(getDocType())) { return (result, toDocument); } toDocument[PrimaryKey.PK_Define] = getPK(); toDocument[PrimaryKey.SK_Define] = getSK(); if (isEnableTTL()) { toDocument[DynamoDbClient.TTL_NAME] = getTTLSeconds(); } toDocument["DocType"] = getDocType(); foreach (var each in m_attrib_wrappers) { each.Value.onCopyToDynamoDbEntry(ref toDocument); } if(true == getCreatedDateTime().IsSet) { toDocument[DynamoDbDocBase.CreatedDateTime] = getCreatedDateTime().ProcessedTime; } if (true == getUpdatedDateTime().IsSet) { toDocument[DynamoDbDocBase.UpdatedDateTime] = getUpdatedDateTime().ProcessedTime; } if (true == getDeletedDateTime().IsSet) { toDocument[DynamoDbDocBase.DeletedDateTime] = getDeletedDateTime().ProcessedTime; } if (true == getRestoredDateTime().IsSet) { toDocument[DynamoDbDocBase.RestoredDateTime] = getRestoredDateTime().ProcessedTime; } return (result, toDocument); } catch(Exception e) { err_msg = $"Exception !!!, Failed to perform in DynamoDbDocBase.onCopyToDocument() !!! : exception:{e} - docType:{doc_type}, PK:{pk}, SK:{sk}"; result.setFail(ServerErrorCode.DynamoDbDocCopyFailedToDocument, err_msg); Log.getLogger().fatal(err_msg); return (result, toDocument); } }); } public void copyTimestampsFromOriginDocBase(DynamoDbDocBase originDocBase) { setCreatedDateTime(originDocBase.getCreatedDateTime().ProcessedTime); setUpdatedDateTime(originDocBase.getUpdatedDateTime().ProcessedTime); setDeletedDateTime(originDocBase.getDeletedDateTime().ProcessedTime); setRestoredDateTime(originDocBase.getRestoredDateTime().ProcessedTime); } protected abstract string onGetPrefixOfPK(); protected virtual string onMakePK() { return $"{onGetPrefixOfPK()}{getCombinationKeyForPK()}"; } protected virtual string onGetPrefixOfSK() { return ""; } protected virtual string onMakeSK() { return getCombinationKeyForSK(); } protected ServerErrorCode fillUpPrimaryKey(string partitionKey, string sortKey) { var err_msg = string.Empty; var error_code = onCheckAndSetPK(partitionKey); if (error_code.isFail()) { return error_code; } if(0 < sortKey.Length) { if(true == sortKey.isEmptySK()) { getPrimaryKey().setEmptySK(); } else { error_code = onCheckAndSetSK(sortKey); if (error_code.isFail()) { err_msg = $"Failed to onCheckAndSetSK() !!!, param sortKey exist : sorkKey:{sortKey}, {error_code.toBasicString()} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return error_code; } } } return ServerErrorCode.Success; } protected virtual ServerErrorCode onCheckAndSetPK(string partitionKey) { var error_code = onFillupCombinationKeyForPK(partitionKey, out var combination_key_for_pk); if (error_code.isFail()) { return error_code; } getPrimaryKey().fillUpPK(partitionKey); setCombinationKeyForPK(combination_key_for_pk); return ServerErrorCode.Success; } public virtual ServerErrorCode onFillupCombinationKeyForPK(string partitionKey, out string fillupKey) { fillupKey = string.Empty; var prefix_of_pk = onGetPrefixOfPK(); if (true == prefix_of_pk.isNullOrWhiteSpace()) { fillupKey = partitionKey; } else { var combination_key_for_pk = partitionKey.Replace(onGetPrefixOfPK(), ""); if (0 >= combination_key_for_pk.Length) { var err_msg = $"PK combination 0 is length !!! : PrimaryKey:{partitionKey} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return ServerErrorCode.DynamoDbDocPkInvalid; } fillupKey = combination_key_for_pk; } return ServerErrorCode.Success; } protected virtual ServerErrorCode onCheckAndSetSK(string sortKey) { return ServerErrorCode.FunctionNotImplemented; } public virtual ServerErrorCode onFillupCombinationKeyForSK(string sortKey, out string fillupKey) { fillupKey = string.Empty; var prefix_of_sk = onGetPrefixOfSK(); if(true == prefix_of_sk.isNullOrWhiteSpace()) { fillupKey = sortKey; } else { var combination_key_for_sk = sortKey.Replace(prefix_of_sk, ""); if (0 >= combination_key_for_sk.Length) { var err_msg = $"SK combination 0 is length !!! : SortKey:{sortKey} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return ServerErrorCode.DynamoDbDocSkInvalid; } fillupKey = combination_key_for_sk; } return ServerErrorCode.Success; } public virtual ServerErrorCode onApplyPKSK() { return fillUpPrimaryKey(onMakePK(), onMakeSK()); } protected Int64 getTTLSeconds() => m_ttl_seconds; protected virtual Task onPrepareCRUD() { return Task.FromResult(new Result()); } private bool fillUpDocType(string docType) { var err_msg = string.Empty; if ( false == m_doc_type.Equals(docType, StringComparison.OrdinalIgnoreCase) ) { err_msg = $"DocType Not matched !!! : currDocType:{m_doc_type} == reqDocType:{docType} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return false; } return true; } public TAttrib? getAttrib() where TAttrib : AttribBase { var err_msg = string.Empty; var to_find_attrib_type = typeof(TAttrib); if (false == m_attrib_wrappers.TryGetValue(to_find_attrib_type, out var lookup_attrib)) { err_msg = $"Not found AttribType !!! : toFindAttribType:{to_find_attrib_type.Name} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return null; } var found_attrib = lookup_attrib.getAttribBase() as TAttrib; if (null == found_attrib) { err_msg = $"Failed to cast AttribType : toCastAttribType:{to_find_attrib_type.Name} == lookupAttribType:{lookup_attrib.getAttribType().Name} - {toBasicString()}"; Log.getLogger().fatal(err_msg); return null; } return found_attrib; } protected void setCreatedDateTime(DateTime currentTime) { m_created_datetime.setProcessedTime(currentTime); } protected void setUpdatedDateTime(DateTime currentTime) { m_updated_datetime.setProcessedTime(currentTime); } protected void changeUpdatedDateTime(DateTime currentTime) { m_updated_datetime.setProcessedTime(currentTime); } protected void setDeletedDateTime(DateTime currentTime) { m_deleted_datetime.setProcessedTime(currentTime); } protected void setRestoredDateTime(DateTime currentTime) { m_restored_datetime.setProcessedTime(currentTime); } public virtual string toAttribAllString() { var attrib_string = string.Empty; foreach(var attrib_wrapper in m_attrib_wrappers.Values) { attrib_string += attrib_wrapper.getAttribBase().toBasicString(); attrib_string += ", "; } var found_index = attrib_string.LastIndexOf(", "); if(found_index != -1) { attrib_string.Remove(found_index); } return attrib_string; } protected bool isEnableTTL() => m_is_enable_ttl; public void desableTTL() { m_ttl_seconds = 0; m_is_enable_ttl = false; } public void enableTTL(long ttlSeconds) { m_ttl_seconds = ttlSeconds; m_is_enable_ttl = true; } public string toDocTypePKSK() { return $"{this.getTypeName()}:{getPrimaryKey().toBasicString()}"; } public string toJsonStringOfAttribs() { var json_object = new JObject(); foreach (var each in m_attrib_wrappers) { if(false == each.Value.onFillupJsonStringToJObject(ref json_object)) { var err_msg = $"Failed to onFillupJsonStringToDynamoDbEntry() !!! : {each.Value.getAttribType().Name} - {toBasicString()}"; Log.getLogger().fatal(err_msg); } } return JsonConvert.SerializeObject(json_object, Formatting.Indented); } public virtual string toBasicString() { return $"{this.getTypeName()}: tableName:{TableName}, {getPrimaryKey().toBasicString()}" + $", {getCreatedDateTime().ProcessedTime.toString("o")}" + $", {getUpdatedDateTime().ProcessedTime.toString("o")}" + $", {getDeletedDateTime().ProcessedTime.toString("o")}" + $", {getRestoredDateTime().ProcessedTime.toString("o")}"; } }