- 优化 集合导航属性表达式中忘记使用 AsSelect() 的友好错误提示;

This commit is contained in:
28810 2020-03-26 23:43:25 +08:00
parent 58aa99a6e6
commit ff61607e01
15 changed files with 60 additions and 165 deletions

View File

@ -550,7 +550,6 @@ namespace FreeSql.Tests
var gkjdjd = g.sqlite.Select<AuthorTest>().Where(a => a.Post.AsSelect().Count() > 0).ToList(); var gkjdjd = g.sqlite.Select<AuthorTest>().Where(a => a.Post.AsSelect().Count() > 0).ToList();
var testrunsql1 = g.mysql.Select<TaskBuild>().Where(a => a.OptionsEntity04 > DateTime.Now.AddDays(0).ToString("yyyyMMdd").TryTo<int>()).ToSql(); var testrunsql1 = g.mysql.Select<TaskBuild>().Where(a => a.OptionsEntity04 > DateTime.Now.AddDays(0).ToString("yyyyMMdd").TryTo<int>()).ToSql();
@ -892,8 +891,6 @@ namespace FreeSql.Tests
var ttt1 = g.sqlite.Select<Model1>().Where(a => a.Childs.AsSelect().Any(b => b.Title == "111")).ToList(); var ttt1 = g.sqlite.Select<Model1>().Where(a => a.Childs.AsSelect().Any(b => b.Title == "111")).ToList();

View File

@ -51,18 +51,15 @@ public static partial class FreeSqlGlobalExtensions
/// <summary> /// <summary>
/// 获取 Type 的原始 c# 文本表示 /// 获取 Type 的原始 c# 文本表示
/// </summary> /// </summary>
/// <param name="that"></param> /// <param name="type"></param>
/// <returns></returns> /// <returns></returns>
public static string DisplayCsharp(this Type that) internal static string DisplayCsharp(this Type type, bool isNameSpace = true)
{ {
return DisplayCsharp(that, true); if (type == null) return null;
} if (type == typeof(void)) return "void";
static string DisplayCsharp(this Type that, bool isNameSpace) if (type.IsGenericParameter) return type.Name;
{
if (that == typeof(void)) return "void";
if (that.IsGenericParameter) return that.Name;
var sb = new StringBuilder(); var sb = new StringBuilder();
var nestedType = that; var nestedType = type;
while (nestedType.IsNested) while (nestedType.IsNested)
{ {
sb.Insert(0, ".").Insert(0, DisplayCsharp(nestedType.DeclaringType, false)); sb.Insert(0, ".").Insert(0, DisplayCsharp(nestedType.DeclaringType, false));
@ -71,22 +68,22 @@ public static partial class FreeSqlGlobalExtensions
if (isNameSpace && string.IsNullOrEmpty(nestedType.Namespace) == false) if (isNameSpace && string.IsNullOrEmpty(nestedType.Namespace) == false)
sb.Insert(0, ".").Insert(0, nestedType.Namespace); sb.Insert(0, ".").Insert(0, nestedType.Namespace);
if (that.IsGenericType == false) if (type.IsGenericType == false)
return sb.Append(that.Name).ToString(); return sb.Append(type.Name).ToString();
var genericParameters = that.GetGenericArguments(); var genericParameters = type.GetGenericArguments();
if (that.IsNested && that.DeclaringType.IsGenericType) if (type.IsNested && type.DeclaringType.IsGenericType)
{ {
var dic = genericParameters.ToDictionary(a => a.Name); var dic = genericParameters.ToDictionary(a => a.Name);
foreach (var nestedGenericParameter in that.DeclaringType.GetGenericArguments()) foreach (var nestedGenericParameter in type.DeclaringType.GetGenericArguments())
if (dic.ContainsKey(nestedGenericParameter.Name)) if (dic.ContainsKey(nestedGenericParameter.Name))
dic.Remove(nestedGenericParameter.Name); dic.Remove(nestedGenericParameter.Name);
genericParameters = dic.Values.ToArray(); genericParameters = dic.Values.ToArray();
} }
if (genericParameters.Any() == false) if (genericParameters.Any() == false)
return sb.Append(that.Name).ToString(); return sb.Append(type.Name).ToString();
sb.Append(that.Name.Remove(that.Name.IndexOf('`'))).Append("<"); sb.Append(type.Name.Remove(type.Name.IndexOf('`'))).Append("<");
var genericTypeIndex = 0; var genericTypeIndex = 0;
foreach (var genericType in genericParameters) foreach (var genericType in genericParameters)
{ {
@ -95,6 +92,37 @@ public static partial class FreeSqlGlobalExtensions
} }
return sb.Append(">").ToString(); return sb.Append(">").ToString();
} }
internal static string DisplayCsharp(this MethodInfo method, bool isOverride)
{
if (method == null) return null;
var sb = new StringBuilder();
if (method.IsPublic) sb.Append("public ");
if (method.IsAssembly) sb.Append("internal ");
if (method.IsFamily) sb.Append("protected ");
if (method.IsPrivate) sb.Append("private ");
if (method.IsPrivate) sb.Append("private ");
if (method.IsStatic) sb.Append("static ");
if (method.IsAbstract && method.DeclaringType.IsInterface == false) sb.Append("abstract ");
if (method.IsVirtual && method.DeclaringType.IsInterface == false) sb.Append(isOverride ? "override " : "virtual ");
sb.Append(method.ReturnType.DisplayCsharp()).Append(" ").Append(method.Name);
var genericParameters = method.GetGenericArguments();
if (method.DeclaringType.IsNested && method.DeclaringType.DeclaringType.IsGenericType)
{
var dic = genericParameters.ToDictionary(a => a.Name);
foreach (var nestedGenericParameter in method.DeclaringType.DeclaringType.GetGenericArguments())
if (dic.ContainsKey(nestedGenericParameter.Name))
dic.Remove(nestedGenericParameter.Name);
genericParameters = dic.Values.ToArray();
}
if (genericParameters.Any())
sb.Append("<")
.Append(string.Join(", ", genericParameters.Select(a => a.DisplayCsharp())))
.Append(">");
sb.Append("(").Append(string.Join(", ", method.GetParameters().Select(a => $"{a.ParameterType.DisplayCsharp()} {a.Name}"))).Append(")");
return sb.ToString();
}
public static object CreateInstanceGetDefaultValue(this Type that) public static object CreateInstanceGetDefaultValue(this Type that)
{ {
if (that == null) return null; if (that == null) return null;

View File

@ -2285,137 +2285,6 @@
<param name="parms"></param> <param name="parms"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:FreeSql.IAdo.ExecuteReaderAsync(System.Func{System.Data.Common.DbDataReader,System.Threading.Tasks.Task},System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
查询若使用读写分离查询【从库】条件cmdText.StartsWith("SELECT "),否则查询【主库】
</summary>
<param name="readerHander"></param>
<param name="cmdType"></param>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteReaderAsync(System.Func{System.Data.Common.DbDataReader,System.Threading.Tasks.Task},System.String,System.Object)">
<summary>
查询ExecuteReaderAsync(dr => {}, "select * from user where age > ?age", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteArrayAsync(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
查询
</summary>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteArrayAsync(System.String,System.Object)">
<summary>
查询ExecuteArrayAsync("select * from user where age > ?age", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.ExecuteDataSetAsync(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
查询
</summary>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteDataSetAsync(System.String,System.Object)">
<summary>
查询ExecuteDataSetAsync("select * from user where age > ?age; select 2", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.ExecuteDataTableAsync(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
查询
</summary>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteDataTableAsync(System.String,System.Object)">
<summary>
查询ExecuteDataTableAsync("select * from user where age > ?age", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.ExecuteNonQueryAsync(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
在【主库】执行
</summary>
<param name="cmdType"></param>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteNonQueryAsync(System.String,System.Object)">
<summary>
在【主库】执行ExecuteNonQueryAsync("delete from user where age > ?age", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.ExecuteScalarAsync(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
在【主库】执行
</summary>
<param name="cmdType"></param>
<param name="cmdText"></param>
<param name="cmdParms"></param>
</member>
<member name="M:FreeSql.IAdo.ExecuteScalarAsync(System.String,System.Object)">
<summary>
在【主库】执行ExecuteScalarAsync("select 1 from user where age > ?age", new { age = 25 })
</summary>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.QueryAsync``1(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
执行SQL返回对象集合QueryAsync&lt;User&gt;("select * from user where age > ?age", new SqlParameter { ParameterName = "age", Value = 25 })
</summary>
<typeparam name="T"></typeparam>
<param name="cmdType"></param>
<param name="cmdText"></param>
<param name="cmdParms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.QueryAsync``1(System.String,System.Object)">
<summary>
执行SQL返回对象集合QueryAsync&lt;User&gt;("select * from user where age > ?age", new { age = 25 })
</summary>
<typeparam name="T"></typeparam>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.QueryAsync``2(System.Data.CommandType,System.String,System.Data.Common.DbParameter[])">
<summary>
执行SQL返回对象集合Query&lt;User&gt;("select * from user where age > ?age; select * from address", new SqlParameter { ParameterName = "age", Value = 25 })
</summary>
<typeparam name="T1"></typeparam>
<param name="cmdType"></param>
<param name="cmdText"></param>
<param name="cmdParms"></param>
<returns></returns>
</member>
<member name="M:FreeSql.IAdo.QueryAsync``2(System.String,System.Object)">
<summary>
执行SQL返回对象集合Query&lt;User&gt;("select * from user where age > ?age; select * from address", new { age = 25 })
</summary>
<typeparam name="T1"></typeparam>
<param name="cmdText"></param>
<param name="parms"></param>
<returns></returns>
</member>
<member name="E:FreeSql.IAop.ParseExpression"> <member name="E:FreeSql.IAop.ParseExpression">
<summary> <summary>
可自定义解析表达式 可自定义解析表达式
@ -2939,12 +2808,6 @@
<param name="timeout">超时</param> <param name="timeout">超时</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:FreeSql.Internal.ObjectPool.IObjectPool`1.GetAsync">
<summary>
获取资源
</summary>
<returns></returns>
</member>
<member name="M:FreeSql.Internal.ObjectPool.IObjectPool`1.Return(FreeSql.Internal.ObjectPool.Object{`0},System.Boolean)"> <member name="M:FreeSql.Internal.ObjectPool.IObjectPool`1.Return(FreeSql.Internal.ObjectPool.Object{`0},System.Boolean)">
<summary> <summary>
使用完毕后,归还资源 使用完毕后,归还资源
@ -3015,12 +2878,6 @@
</summary> </summary>
<param name="obj">资源对象</param> <param name="obj">资源对象</param>
</member> </member>
<member name="M:FreeSql.Internal.ObjectPool.IPolicy`1.OnGetAsync(FreeSql.Internal.ObjectPool.Object{`0})">
<summary>
从对象池获取对象成功的时候触发,通过该方法统计或初始化对象
</summary>
<param name="obj">资源对象</param>
</member>
<member name="M:FreeSql.Internal.ObjectPool.IPolicy`1.OnReturn(FreeSql.Internal.ObjectPool.Object{`0})"> <member name="M:FreeSql.Internal.ObjectPool.IPolicy`1.OnReturn(FreeSql.Internal.ObjectPool.Object{`0})">
<summary> <summary>
归还对象给对象池的时候触发 归还对象给对象池的时候触发
@ -3194,11 +3051,11 @@
<param name="end"></param> <param name="end"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:FreeSqlGlobalExtensions.DisplayCsharp(System.Type)"> <member name="M:FreeSqlGlobalExtensions.DisplayCsharp(System.Type,System.Boolean)">
<summary> <summary>
获取 Type 的原始 c# 文本表示 获取 Type 的原始 c# 文本表示
</summary> </summary>
<param name="that"></param> <param name="type"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:FreeSqlGlobalExtensions.Distance(System.Drawing.Point,System.Drawing.Point)"> <member name="M:FreeSqlGlobalExtensions.Distance(System.Drawing.Point,System.Drawing.Point)">

View File

@ -1021,6 +1021,7 @@ namespace FreeSql.Internal
other3Exp = ExpressionLambdaToSqlOther(exp3, tsc); other3Exp = ExpressionLambdaToSqlOther(exp3, tsc);
if (string.IsNullOrEmpty(other3Exp) == false) return other3Exp; if (string.IsNullOrEmpty(other3Exp) == false) return other3Exp;
if (exp3.IsParameter() == false) return formatSql(Expression.Lambda(exp3).Compile().DynamicInvoke(), tsc.mapType, tsc.mapColumnTmp, tsc.dbParams); if (exp3.IsParameter() == false) return formatSql(Expression.Lambda(exp3).Compile().DynamicInvoke(), tsc.mapType, tsc.mapColumnTmp, tsc.dbParams);
if (exp3.Method.DeclaringType == typeof(Enumerable)) throw new Exception($"未实现函数表达式 {exp3} 解析。如果正在操作导航属性集合,请使用 .AsSelect().{exp3.Method.Name}({(exp3.Arguments.Count > 1 ? "..." : "")})");
throw new Exception($"未实现函数表达式 {exp3} 解析"); throw new Exception($"未实现函数表达式 {exp3} 解析");
case ExpressionType.Parameter: case ExpressionType.Parameter:
case ExpressionType.MemberAccess: case ExpressionType.MemberAccess:
@ -1292,6 +1293,8 @@ namespace FreeSql.Internal
} }
if (tb2.ColumnsByCsIgnore.ContainsKey(mp2.Member.Name)) if (tb2.ColumnsByCsIgnore.ContainsKey(mp2.Member.Name))
throw new ArgumentException($"{tb2.DbName}.{mp2.Member.Name} 被忽略,请检查 IsIgnore 设置,确认 get/set 为 public"); throw new ArgumentException($"{tb2.DbName}.{mp2.Member.Name} 被忽略,请检查 IsIgnore 设置,确认 get/set 为 public");
if (tb2.GetTableRef(mp2.Member.Name, false) != null)
throw new ArgumentException($"{tb2.DbName}.{mp2.Member.Name} 导航属性集合忘了 .AsSelect() 吗?如果在 ToList(a => a.{mp2.Member.Name}) 中使用,请移步参考 IncludeMany 文档。");
throw new ArgumentException($"{tb2.DbName} 找不到列 {mp2.Member.Name}"); throw new ArgumentException($"{tb2.DbName} 找不到列 {mp2.Member.Name}");
} }
var col2 = tb2.ColumnsByCs[mp2.Member.Name]; var col2 = tb2.ColumnsByCs[mp2.Member.Name];

View File

@ -74,6 +74,7 @@ namespace FreeSql.MsAccess
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -101,6 +101,7 @@ namespace FreeSql.MySql
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -105,6 +105,7 @@ namespace FreeSql.Odbc.Dameng
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -108,6 +108,7 @@ namespace FreeSql.Odbc.Default
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -101,6 +101,7 @@ namespace FreeSql.Odbc.MySql
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -105,6 +105,7 @@ namespace FreeSql.Odbc.Oracle
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -105,6 +105,7 @@ namespace FreeSql.Odbc.SqlServer
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -105,6 +105,7 @@ namespace FreeSql.Oracle
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -105,6 +105,7 @@ namespace FreeSql.SqlServer
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -101,6 +101,7 @@ namespace FreeSql.Sqlite
if (objType == null) objType = callExp.Method.DeclaringType; if (objType == null) objType = callExp.Method.DeclaringType;
if (objType != null || objType.IsArrayOrList()) if (objType != null || objType.IsArrayOrList())
{ {
if (argIndex >= callExp.Arguments.Count) break;
tsc.SetMapColumnTmp(null); tsc.SetMapColumnTmp(null);
var args1 = getExp(callExp.Arguments[argIndex]); var args1 = getExp(callExp.Arguments[argIndex]);
var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp); var oldMapType = tsc.SetMapTypeReturnOld(tsc.mapTypeTmp);

View File

@ -206,7 +206,7 @@ Elapsed: 00:00:00.6495301; Query Entity Counts: 131072; ORM: Dapper
## Donation ## Donation
L*y 58元、花花 88元、麦兜很乖 50元、网络来者 2000元、John 99.99元、alex 666元 L*y 58元、花花 88元、麦兜很乖 50元、网络来者 2000元、John 99.99元、alex 666元、bacongao 36元
> Thank you for your donation > Thank you for your donation