Guest User

Model.RiaServices.tt

a guest
Jan 13th, 2013
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 20.34 KB | None | 0 0
  1. <#@ template debug="false" hostSpecific="true" #>
  2. <#@ include file="EF.Utility.CS.ttinclude"#>
  3. <#@ output extension=".cs" #>
  4. <#@ Assembly Name="System.Core" #>
  5. <#@ Assembly Name="System.Data.Entity" #>
  6. <#@ Assembly Name="$(SolutionDir)packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll" #>
  7. <#@ import namespace="System.Linq" #>
  8. <#@ import namespace="System.Collections.Generic" #>
  9. <#@ import namespace="System.Data.Entity.Infrastructure" #>
  10. <#@ import namespace="System.Data.Metadata.Edm" #>
  11. <#@ import namespace="System.Data.Entity" #>
  12. <#
  13.  
  14. // settings start
  15. string inputFile = @"%project%Model.edmx";
  16. // settings end
  17.  
  18. var textTransform = DynamicTextTransformation.Create(this);
  19. var code = new CodeGenerationTools(this);
  20. var ef = new MetadataTools(this);
  21. var typeMapper = new TypeMapper(code, ef, textTransform.Errors);
  22. var itemCollection = new EdmMetadataLoader(textTransform.Host, textTransform.Errors).CreateEdmItemCollection(inputFile);
  23.  
  24. Generate(itemCollection, code, typeMapper);
  25. #><#+
  26. void Generate(IEnumerable<GlobalItem> itemCollection, CodeGenerationTools code, TypeMapper typeMapper)
  27. {
  28.     ArgumentNotNull(itemCollection, "itemCollection");
  29.     ArgumentNotNull(code, "code");
  30.  
  31.     var codeNamespace = code.VsNamespaceSuggestion();
  32. #>
  33. #pragma warning disable 649    // disable compiler warnings about unassigned fields
  34.  
  35. namespace <#= code.EscapeNamespace(codeNamespace) #>
  36. {
  37.     using System;
  38.     using System.Collections.Generic;
  39.     using System.ComponentModel;
  40.     using System.ComponentModel.DataAnnotations;
  41.     using System.Data.Objects.DataClasses;
  42.     using System.Linq;
  43.     using System.Runtime.Serialization;
  44.     using System.ServiceModel.DomainServices.Hosting;
  45.     using System.ServiceModel.DomainServices.Server;
  46.     using RIAServices.M2M;
  47.  
  48. <#+
  49.     var container = itemCollection.OfType<EntityContainer>().First();
  50.     var associations = container.BaseEntitySets
  51.         .Where(es => es.BuiltInTypeKind == BuiltInTypeKind.EntitySet)
  52.         .OfType<EntitySet>()
  53.         .SelectMany(es => es.ElementType.NavigationProperties)
  54.         .Where(np => np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many && np.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
  55.     var entities = itemCollection.OfType<EntityType>();
  56.     foreach (var elementType in entities)//container.BaseEntitySets.Where(set => set.BuiltInTypeKind == BuiltInTypeKind.EntitySet))
  57.     {
  58.         string elementName = code.Escape(elementType.Name);
  59.         string summary = elementType.Documentation != null ? elementType.Documentation.Summary : null;
  60.         if (!string.IsNullOrEmpty(summary))
  61.         {
  62. #>
  63.     /// <summary>
  64.     /// <#= summary #>
  65.     /// </summary>
  66. <#+
  67.         }
  68.         // http://msdn.microsoft.com/en-us/library/ee707366%28v=vs.91%29.aspx
  69.         var knownTypes = entities.Where(e => e.BaseType == elementType);
  70.         if (knownTypes.Count() > 0)
  71.         {
  72.             string knownLine = "";
  73.             var last = knownTypes.Last();
  74.             foreach (var knownType in knownTypes)
  75.             {
  76.                 knownLine += "KnownType(typeof(" + code.Escape(knownType.Name) + "))";
  77.                 if (knownType != last)
  78.                     knownLine += ", ";
  79.             }
  80. #>
  81.     [<#= knownLine #>]
  82. <#+
  83.         }
  84. #>
  85.     [MetadataTypeAttribute(typeof(<#= elementName #>.<#= elementName #>Metadata))]
  86.     public partial class <#= elementName #>
  87.     {
  88.         internal sealed class <#= elementName #>Metadata
  89.         {
  90.             private <#= elementName #>Metadata() { }
  91. <#+
  92.         var members = elementType.Members.AsEnumerable();
  93.         if (elementType.BaseType != null && elementType.BaseType is StructuralType)
  94.             members = members.Except(((StructuralType)elementType.BaseType).Members);
  95.         foreach (var member in members)
  96.         {
  97.             WriteLine("");
  98.             summary = member.Documentation != null ? member.Documentation.Summary : null;
  99.             if (!string.IsNullOrEmpty(summary))
  100.             {
  101. #>
  102.             [Display(Name="<#= summary #>")]
  103. <#+
  104.             }
  105.             if (elementType.KeyMembers.Contains(member))
  106.             {
  107. #>
  108.             [Key]
  109. <#+
  110.             }
  111.             if (member.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType || member.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.CollectionType)
  112.             {
  113.                 if (!associations.Contains(member))
  114.                 {
  115. #>
  116.             [Include]
  117. <#+
  118.                 }
  119.             }
  120.            
  121.             string type = typeMapper.GetTypeName(member.TypeUsage);
  122.             if (member is EdmProperty && ((EdmProperty)member).Nullable == false)
  123.             {
  124. #>
  125.             [Required(ErrorMessage="Значение не может быть пустым")]
  126. <#+
  127.             }
  128. #>
  129.             public <#= type #> <#= code.Escape(member.Name) #>;
  130. <#+
  131.         }
  132.         #>
  133.         }
  134.     }
  135. <#+
  136.     }
  137.  
  138.     // M2M support http://m2m4ria.codeplex.com/wikipage?title=Step-by-step%20instructions%20for%20using%20M2M%20with%20WCR%20RIA%20Services
  139.     foreach (var propertiesGroup in associations.GroupBy(np => np.RelationshipType))
  140.     {
  141.         var relationship = propertiesGroup.Key;
  142.         var className = code.Escape("M2M_" + relationship.Name);
  143.         var type1 = relationship.RelationshipEndMembers.First().GetEntityType();
  144.         var type2 = relationship.RelationshipEndMembers.Last().GetEntityType();
  145. #>
  146.     public class <#= className #> : LinkTable<<#= code.Escape(type1.Name) #>, <#= code.Escape(type2.Name) #>> { }
  147. <#+
  148.         foreach (var property in propertiesGroup)
  149.         {
  150. #>
  151.     public partial class <#= code.Escape(property.DeclaringType.Name) #>
  152.     {
  153.         [Include]
  154.         public ICollection<<#= className #>> <#= className #>
  155.         {
  156.             get
  157.             {
  158.                 return <#= code.Escape(property.Name) #>.ToLinkTable<<#= code.Escape(type1.Name) #>, <#= code.Escape(type2.Name) #>, <#= className #>>(this);
  159.             }
  160.         }
  161.     }
  162. <#+
  163.         }
  164.     }
  165. #>
  166. }
  167. #pragma warning restore 649    // re-enable compiler warnings about unassigned fields
  168. <#+
  169. }
  170.  
  171. public class TypeMapper
  172. {
  173.     private const string ExternalTypeNameAttributeName = @"http://schemas.microsoft.com/ado/2006/04/codegeneration:ExternalTypeName";
  174.  
  175.     private readonly System.Collections.IList _errors;
  176.     private readonly CodeGenerationTools _code;
  177.     private readonly MetadataTools _ef;
  178.  
  179.     public TypeMapper(CodeGenerationTools code, MetadataTools ef, System.Collections.IList errors)
  180.     {
  181.         ArgumentNotNull(code, "code");
  182.         ArgumentNotNull(ef, "ef");
  183.         ArgumentNotNull(errors, "errors");
  184.  
  185.         _code = code;
  186.         _ef = ef;
  187.         _errors = errors;
  188.     }
  189.  
  190.     public string GetTypeName(TypeUsage typeUsage)
  191.     {
  192.         return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace: null);
  193.     }
  194.  
  195.     public string GetTypeName(EdmType edmType)
  196.     {
  197.         return GetTypeName(edmType, isNullable: null, modelNamespace: null);
  198.     }
  199.  
  200.     public string GetTypeName(TypeUsage typeUsage, string modelNamespace)
  201.     {
  202.         return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, _ef.IsNullable(typeUsage), modelNamespace);
  203.     }
  204.  
  205.     public string GetTypeName(EdmType edmType, string modelNamespace)
  206.     {
  207.         return GetTypeName(edmType, isNullable: null, modelNamespace: modelNamespace);
  208.     }
  209.  
  210.     public string GetTypeName(EdmType edmType, bool? isNullable, string modelNamespace)
  211.     {
  212.         if (edmType == null)
  213.         {
  214.             return null;
  215.         }
  216.  
  217.         var collectionType = edmType as CollectionType;
  218.         if (collectionType != null)
  219.         {
  220.             return String.Format(CultureInfo.InvariantCulture, "ICollection<{0}>", GetTypeName(collectionType.TypeUsage, modelNamespace));
  221.         }
  222.  
  223.         var typeName = _code.Escape(edmType.MetadataProperties
  224.                                 .Where(p => p.Name == ExternalTypeNameAttributeName)
  225.                                 .Select(p => (string)p.Value)
  226.                                 .FirstOrDefault())
  227.             ?? (modelNamespace != null && edmType.NamespaceName != modelNamespace ?
  228.                 _code.CreateFullName(_code.EscapeNamespace(edmType.NamespaceName), _code.Escape(edmType)) :
  229.                 _code.Escape(edmType));
  230.  
  231.         if (edmType is StructuralType)
  232.         {
  233.             return typeName;
  234.         }
  235.  
  236.         if (edmType is SimpleType)
  237.         {
  238.             var clrType = UnderlyingClrType(edmType);
  239.             if (!IsEnumType(edmType))
  240.             {
  241.                 typeName = _code.Escape(clrType);
  242.             }
  243.  
  244.             return clrType.IsValueType && isNullable == true ?
  245.                 String.Format(CultureInfo.InvariantCulture, "Nullable<{0}>", typeName) :
  246.                 typeName;
  247.         }
  248.  
  249.         throw new ArgumentException("edmType");
  250.     }
  251.    
  252.     public Type UnderlyingClrType(EdmType edmType)
  253.     {
  254.         ArgumentNotNull(edmType, "edmType");
  255.  
  256.         var primitiveType = edmType as PrimitiveType;
  257.         if (primitiveType != null)
  258.         {
  259.             return primitiveType.ClrEquivalentType;
  260.         }
  261.  
  262.         if (IsEnumType(edmType))
  263.         {
  264.             return GetEnumUnderlyingType(edmType).ClrEquivalentType;
  265.         }
  266.  
  267.         return typeof(object);
  268.     }
  269.    
  270.     public object GetEnumMemberValue(MetadataItem enumMember)
  271.     {
  272.         ArgumentNotNull(enumMember, "enumMember");
  273.        
  274.         var valueProperty = enumMember.GetType().GetProperty("Value");
  275.         return valueProperty == null ? null : valueProperty.GetValue(enumMember, null);
  276.     }
  277.    
  278.     public string GetEnumMemberName(MetadataItem enumMember)
  279.     {
  280.         ArgumentNotNull(enumMember, "enumMember");
  281.        
  282.         var nameProperty = enumMember.GetType().GetProperty("Name");
  283.         return nameProperty == null ? null : (string)nameProperty.GetValue(enumMember, null);
  284.     }
  285.  
  286.     public System.Collections.IEnumerable GetEnumMembers(EdmType enumType)
  287.     {
  288.         ArgumentNotNull(enumType, "enumType");
  289.  
  290.         var membersProperty = enumType.GetType().GetProperty("Members");
  291.         return membersProperty != null
  292.             ? (System.Collections.IEnumerable)membersProperty.GetValue(enumType, null)
  293.             : Enumerable.Empty<MetadataItem>();
  294.     }
  295.    
  296.     public bool EnumIsFlags(EdmType enumType)
  297.     {
  298.         ArgumentNotNull(enumType, "enumType");
  299.        
  300.         var isFlagsProperty = enumType.GetType().GetProperty("IsFlags");
  301.         return isFlagsProperty != null && (bool)isFlagsProperty.GetValue(enumType, null);
  302.     }
  303.  
  304.     public bool IsEnumType(GlobalItem edmType)
  305.     {
  306.         ArgumentNotNull(edmType, "edmType");
  307.  
  308.         return edmType.GetType().Name == "EnumType";
  309.     }
  310.  
  311.     public PrimitiveType GetEnumUnderlyingType(EdmType enumType)
  312.     {
  313.         ArgumentNotNull(enumType, "enumType");
  314.  
  315.         return (PrimitiveType)enumType.GetType().GetProperty("UnderlyingType").GetValue(enumType, null);
  316.     }
  317.  
  318.     public string CreateLiteral(object value)
  319.     {
  320.         if (value == null || value.GetType() != typeof(TimeSpan))
  321.         {
  322.             return _code.CreateLiteral(value);
  323.         }
  324.  
  325.         return string.Format(CultureInfo.InvariantCulture, "new TimeSpan({0})", ((TimeSpan)value).Ticks);
  326.     }
  327.    
  328.     public bool VerifyCaseInsensitiveTypeUniqueness(IEnumerable<string> types, string sourceFile)
  329.     {
  330.         ArgumentNotNull(types, "types");
  331.         ArgumentNotNull(sourceFile, "sourceFile");
  332.        
  333.         var hash = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
  334.         if (types.Any(item => !hash.Add(item)))
  335.         {
  336.             _errors.Add(
  337.                 new CompilerError(sourceFile, -1, -1, "6023",
  338.                     String.Format(CultureInfo.CurrentCulture, GetResourceString("Template_CaseInsensitiveTypeConflict"))));
  339.             return false;
  340.         }
  341.         return true;
  342.     }
  343.    
  344.     public IEnumerable<SimpleType> GetEnumItemsToGenerate(IEnumerable<GlobalItem> itemCollection)
  345.     {
  346.         return GetItemsToGenerate<SimpleType>(itemCollection)
  347.             .Where(e => IsEnumType(e));
  348.     }
  349.    
  350.     public IEnumerable<T> GetItemsToGenerate<T>(IEnumerable<GlobalItem> itemCollection) where T: EdmType
  351.     {
  352.         return itemCollection
  353.             .OfType<T>()
  354.             .Where(i => !i.MetadataProperties.Any(p => p.Name == ExternalTypeNameAttributeName))
  355.             .OrderBy(i => i.Name);
  356.     }
  357.  
  358.     public IEnumerable<string> GetAllGlobalItems(IEnumerable<GlobalItem> itemCollection)
  359.     {
  360.         return itemCollection
  361.             .Where(i => i is EntityType || i is ComplexType || i is EntityContainer || IsEnumType(i))
  362.             .Select(g => GetGlobalItemName(g));
  363.     }
  364.  
  365.     public string GetGlobalItemName(GlobalItem item)
  366.     {
  367.         if (item is EdmType)
  368.         {
  369.             return ((EdmType)item).Name;
  370.         }
  371.         else
  372.         {
  373.             return ((EntityContainer)item).Name;
  374.         }
  375.     }
  376.  
  377.     public IEnumerable<EdmProperty> GetSimpleProperties(EntityType type)
  378.     {
  379.         return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
  380.     }
  381.    
  382.     public IEnumerable<EdmProperty> GetSimpleProperties(ComplexType type)
  383.     {
  384.         return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type);
  385.     }
  386.    
  387.     public IEnumerable<EdmProperty> GetComplexProperties(EntityType type)
  388.     {
  389.         return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
  390.     }
  391.    
  392.     public IEnumerable<EdmProperty> GetComplexProperties(ComplexType type)
  393.     {
  394.         return type.Properties.Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == type);
  395.     }
  396.  
  397.     public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(EntityType type)
  398.     {
  399.         return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
  400.     }
  401.    
  402.     public IEnumerable<EdmProperty> GetPropertiesWithDefaultValues(ComplexType type)
  403.     {
  404.         return type.Properties.Where(p => p.TypeUsage.EdmType is SimpleType && p.DeclaringType == type && p.DefaultValue != null);
  405.     }
  406.  
  407.     public IEnumerable<NavigationProperty> GetNavigationProperties(EntityType type)
  408.     {
  409.         return type.NavigationProperties.Where(np => np.DeclaringType == type);
  410.     }
  411.    
  412.     public IEnumerable<NavigationProperty> GetCollectionNavigationProperties(EntityType type)
  413.     {
  414.         return type.NavigationProperties.Where(np => np.DeclaringType == type && np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many);
  415.     }
  416.    
  417.     public FunctionParameter GetReturnParameter(EdmFunction edmFunction)
  418.     {
  419.         ArgumentNotNull(edmFunction, "edmFunction");
  420.  
  421.         var returnParamsProperty = edmFunction.GetType().GetProperty("ReturnParameters");
  422.         return returnParamsProperty == null
  423.             ? edmFunction.ReturnParameter
  424.             : ((IEnumerable<FunctionParameter>)returnParamsProperty.GetValue(edmFunction, null)).FirstOrDefault();
  425.     }
  426.  
  427.     public bool IsComposable(EdmFunction edmFunction)
  428.     {
  429.         ArgumentNotNull(edmFunction, "edmFunction");
  430.  
  431.         var isComposableProperty = edmFunction.GetType().GetProperty("IsComposableAttribute");
  432.         return isComposableProperty != null && (bool)isComposableProperty.GetValue(edmFunction, null);
  433.     }
  434.  
  435.     public IEnumerable<FunctionImportParameter> GetParameters(EdmFunction edmFunction)
  436.     {
  437.         return FunctionImportParameter.Create(edmFunction.Parameters, _code, _ef);
  438.     }
  439.  
  440.     public TypeUsage GetReturnType(EdmFunction edmFunction)
  441.     {
  442.         var returnParam = GetReturnParameter(edmFunction);
  443.         return returnParam == null ? null : _ef.GetElementType(returnParam.TypeUsage);
  444.     }
  445.    
  446.     public bool GenerateMergeOptionFunction(EdmFunction edmFunction, bool includeMergeOption)
  447.     {
  448.         var returnType = GetReturnType(edmFunction);
  449.         return !includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType;
  450.     }
  451. }
  452. public class EdmMetadataLoader
  453. {
  454.     private readonly IDynamicHost _host;
  455.     private readonly System.Collections.IList _errors;
  456.  
  457.     public EdmMetadataLoader(IDynamicHost host, System.Collections.IList errors)
  458.     {
  459.         ArgumentNotNull(host, "host");
  460.         ArgumentNotNull(errors, "errors");
  461.  
  462.         _host = host;
  463.         _errors = errors;
  464.     }
  465.  
  466.     public IEnumerable<GlobalItem> CreateEdmItemCollection(string sourcePath)
  467.     {
  468.         ArgumentNotNull(sourcePath, "sourcePath");
  469.  
  470.         if (!ValidateInputPath(sourcePath))
  471.         {
  472.             return new EdmItemCollection();
  473.         }
  474.  
  475.         var schemaElement = LoadRootElement(_host.ResolvePath(sourcePath));
  476.         if (schemaElement != null)
  477.         {
  478.             using (var reader = schemaElement.CreateReader())
  479.             {
  480.                 IList<EdmSchemaError> errors;
  481.                 var itemCollection = MetadataItemCollectionFactory.CreateEdmItemCollection(new[] { reader }, out errors);
  482.  
  483.                 ProcessErrors(errors, sourcePath);
  484.  
  485.                 return itemCollection;
  486.             }
  487.         }
  488.         return new EdmItemCollection();
  489.     }
  490.  
  491.     public string GetModelNamespace(string sourcePath)
  492.     {
  493.         ArgumentNotNull(sourcePath, "sourcePath");
  494.  
  495.         if (!ValidateInputPath(sourcePath))
  496.         {
  497.             return string.Empty;
  498.         }
  499.  
  500.         var model = LoadRootElement(_host.ResolvePath(sourcePath));
  501.         if (model == null)
  502.         {
  503.             return string.Empty;
  504.         }
  505.  
  506.         var attribute = model.Attribute("Namespace");
  507.         return attribute != null ? attribute.Value : "";
  508.     }
  509.  
  510.     private bool ValidateInputPath(string sourcePath)
  511.     {
  512.         if (sourcePath == "$" + "edmxInputFile" + "$")
  513.         {
  514.             _errors.Add(
  515.                 new CompilerError(_host.TemplateFile ?? sourcePath, 0, 0, string.Empty,
  516.                     GetResourceString("Template_ReplaceVsItemTemplateToken")));
  517.             return false;
  518.         }
  519.  
  520.         return true;
  521.     }
  522.  
  523.     public XElement LoadRootElement(string sourcePath)
  524.     {
  525.         ArgumentNotNull(sourcePath, "sourcePath");
  526.  
  527.         var root = XElement.Load(sourcePath, LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
  528.         return root.Elements()
  529.             .Where(e => e.Name.LocalName == "Runtime")
  530.             .Elements()
  531.             .Where(e => e.Name.LocalName == "ConceptualModels")
  532.             .Elements()
  533.             .Where(e => e.Name.LocalName == "Schema")
  534.             .FirstOrDefault()
  535.                 ?? root;
  536.     }
  537.  
  538.     private void ProcessErrors(IEnumerable<EdmSchemaError> errors, string sourceFilePath)
  539.     {
  540.         foreach (var error in errors)
  541.         {
  542.             _errors.Add(
  543.                 new CompilerError(
  544.                     error.SchemaLocation ?? sourceFilePath,
  545.                     error.Line,
  546.                     error.Column,
  547.                     error.ErrorCode.ToString(CultureInfo.InvariantCulture),
  548.                     error.Message)
  549.                 {
  550.                     IsWarning = error.Severity == EdmSchemaErrorSeverity.Warning
  551.                 });
  552.         }
  553.     }
  554.    
  555.     public bool IsLazyLoadingEnabled(EntityContainer container)
  556.     {
  557.         string lazyLoadingAttributeValue;
  558.         var lazyLoadingAttributeName = MetadataConstants.EDM_ANNOTATION_09_02 + ":LazyLoadingEnabled";
  559.         bool isLazyLoading;
  560.         return !MetadataTools.TryGetStringMetadataPropertySetting(container, lazyLoadingAttributeName, out lazyLoadingAttributeValue)
  561.             || !bool.TryParse(lazyLoadingAttributeValue, out isLazyLoading)
  562.             || isLazyLoading;
  563.     }
  564. }
  565.  
  566. public static void ArgumentNotNull<T>(T arg, string name) where T : class
  567. {
  568.     if (arg == null)
  569.     {
  570.         throw new ArgumentNullException(name);
  571.     }
  572. }
  573.  
  574. private static readonly Lazy<System.Resources.ResourceManager> ResourceManager =
  575.     new Lazy<System.Resources.ResourceManager>(
  576.         () => new System.Resources.ResourceManager("System.Data.Entity.Design", typeof(MetadataItemCollectionFactory).Assembly), isThreadSafe: true);
  577.  
  578. public static string GetResourceString(string resourceName)
  579. {
  580.     ArgumentNotNull(resourceName, "resourceName");
  581.  
  582.     return ResourceManager.Value.GetString(resourceName, null);
  583. }
  584. #>
Advertisement
Add Comment
Please, Sign In to add comment