您现在的位置是:网站首页> .NET Core

.NET Core服务端学习笔记

摘要

.NET Core服务端学习笔记


1.jpg


ASP.NET Core 实现web应用,不采用MVC方式,用原始的SQL语句,按要求编写一个简单例子

Scriban:高效、强大的.NET开源模板引擎

点击查看微软ASP.NET Core MVC在线学习文档

.NET SDK及运行环境下载

Net Core 2022视频教程

Maui Blazor windows程序无法通过双击 bin 文件夹中的 exe打开程序的解决办法

MAUI桌面端标题栏设置和窗口调整

C# Action和Func的用法详解

MVC与AOP开发笔记

在ASP.NET Core中使用Redis作为Session



ASP.NET Core 实现web应用,不采用MVC方式,用原始的SQL语句,按要求编写一个简单例子


页面直接返回串

页面返回通过字符串模版

页面返回通过模版文件

修改为用页面模版引擎


创建项目

dotnet new web -n RawSqlDemo

cd RawSqlDemo


添加包(仅 SQLite ADO.NET 驱动)

dotnet add package Microsoft.Data.Sqlite


准备数据库

项目根目录下放一个 app.db(空文件即可),第一次运行时会自动建表。

如果你愿意,也可以用 sqlite3 app.db 手动执行:

CREATE TABLE IF NOT EXISTS demo (id INTEGER PRIMARY KEY, msg TEXT);

INSERT INTO demo (msg) VALUES ('Hello from raw SQL');


修改 Program.cs


using Microsoft.Data.Sqlite;


var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();


// 连接字符串指向项目根目录的 app.db

var connStr = "Data Source=app.db";


// 建库建表(若已存在则跳过)

using (var initConn = new SqliteConnection(connStr))

{

    initConn.Open();

    var cmd = initConn.CreateCommand();

    cmd.CommandText =

    @"CREATE TABLE IF NOT EXISTS demo (

        id INTEGER PRIMARY KEY,

        msg TEXT

      );";

    cmd.ExecuteNonQuery();

}


// 路由:GET /time

app.MapGet("/time", async () =>

{

    await using var conn = new SqliteConnection(connStr);

    await conn.OpenAsync();


    // 1) 取 SQLite 当前时间

    var cmdTime = conn.CreateCommand();

    cmdTime.CommandText = "SELECT datetime('now')";

    var dbNow = (string)await cmdTime.ExecuteScalarAsync()!;


    // 2) 再取一条数据证明 SELECT 工作

    var cmdMsg = conn.CreateCommand();

    cmdMsg.CommandText = "SELECT msg FROM demo LIMIT 1";

    var msgObj = await cmdMsg.ExecuteScalarAsync();

    var msg = msgObj?.ToString() ?? "(empty)";


    // 3) 纯文本返回

    return $"Database says: {msg}\nServer time: {dbNow}";

});


// 根路径简单说明

app.MapGet("/", () => "Visit /time to see raw SQL result.");


app.Run();


运行

dotnet run

浏览器访问 https://localhost:<端口>/time 会看到类似:



修改路由Get/Time 通过页面模版返回页面页面内显示时间

using Microsoft.Data.Sqlite;


var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();


var connStr = "Data Source=app.db";


// 建表(同前)

using (var initConn = new SqliteConnection(connStr))

{

    initConn.Open();

    var cmd = initConn.CreateCommand();

    cmd.CommandText =

    @"CREATE TABLE IF NOT EXISTS demo (

        id INTEGER PRIMARY KEY,

        msg TEXT

      );";

    cmd.ExecuteNonQuery();

}


// 纯文本模板(也可放在单独 *.html 文件再读文件)

string TimePageTemplate(string dbTime, string msg) => $@"

<!DOCTYPE html>

<html>

<head>

    <meta charset='utf-8' />

    <title>Raw SQL Time</title>

    <style>

        body {{ font-family: Arial; margin: 40px; }}

        h1   {{ color: #0066cc; }}

    </style>

</head>

<body>

    <h1>数据库当前时间</h1>

    <p><strong>{dbTime}</strong></p>

    <p>附:demo 表内容 = {msg}</p>

</body>

</html>";


// GET /time  => 返回完整 HTML

app.MapGet("/time", async () =>

{

    await using var conn = new SqliteConnection(connStr);

    await conn.OpenAsync();


    var cmdTime = conn.CreateCommand();

    cmdTime.CommandText = "SELECT datetime('now')";

    var dbTime = (string)await cmdTime.ExecuteScalarAsync()!;


    var cmdMsg = conn.CreateCommand();

    cmdMsg.CommandText = "SELECT msg FROM demo LIMIT 1";

    var msg = (await cmdMsg.ExecuteScalarAsync())?.ToString() ?? "(empty)";


    // 返回 text/html

    return Results.Content(TimePageTemplate(dbTime, msg), "text/html");

});


app.MapGet("/", () => "Visit /time to see the templated page.");


app.Run();


修改页面模版采用文件方式提供

在项目根目录建一个 Templates 文件夹

新建文件 Templates/TimeTemplate.html

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8" />

    <title>Raw SQL Time</title>

    <style>

        body { font-family: Arial; margin: 40px; }

        h1   { color: #0066cc; }

    </style>

</head>

<body>

    <h1>数据库当前时间</h1>

    <p><strong>{{DB_TIME}}</strong></p>

    <p>附:demo 表内容 = {{MSG}}</p>

</body>

</html>


修改 Program.cs

using Microsoft.Data.Sqlite;


var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();


var connStr = "Data Source=app.db";


// 1. 初始化数据库(同前)

using (var initConn = new SqliteConnection(connStr))

{

    initConn.Open();

    var cmd = initConn.CreateCommand();

    cmd.CommandText =

    @"CREATE TABLE IF NOT EXISTS demo (

        id INTEGER PRIMARY KEY,

        msg TEXT

      );";

    cmd.ExecuteNonQuery();

}


// 2. 读取模板文件(一次性缓存到内存,也可每次读)

string templatePath = Path.Combine("Templates", "TimeTemplate.html");

string templateHtml = File.ReadAllText(templatePath);


// 3. GET /time

app.MapGet("/time", async () =>

{

    await using var conn = new SqliteConnection(connStr);

    await conn.OpenAsync();


    var cmdTime = conn.CreateCommand();

    cmdTime.CommandText = "SELECT datetime('now')";

    var dbTime = (string)await cmdTime.ExecuteScalarAsync()!;


    var cmdMsg = conn.CreateCommand();

    cmdMsg.CommandText = "SELECT msg FROM demo LIMIT 1";

    var msg = (await cmdMsg.ExecuteScalarAsync())?.ToString() ?? "(empty)";


    // 替换占位符

    var html = templateHtml

               .Replace("{{DB_TIME}}", dbTime)

               .Replace("{{MSG}}", msg);


    return Results.Content(html, "text/html");

});


app.MapGet("/", () => "Visit /time to see the templated page.");


app.Run();



修改为用页面模版引擎

“自己读取文件再 Replace” 改成 真正的模板引擎——使用 Scriban(轻量、无 Razor、无 MVC)。

安装包

dotnet add package Scriban


模板文件 Templates/TimeTemplate.sbnhtml

<!DOCTYPE html>

<html>

<head>

    <meta charset="utf-8" />

    <title>Raw SQL Time</title>

    <style>

        body { font-family: Arial; margin: 40px; }

        h1   { color: #0066cc; }

    </style>

</head>

<body>

    <h1>数据库当前时间</h1>

    <p><strong>{{ db_time }}</strong></p>

    <p>附:demo 表内容 = {{ msg }}</p>

</body>

</html>


修改 Program.cs(核心变化:用 Scriban 解析模板)


using Microsoft.Data.Sqlite;

using Scriban;          // <-- 引入 Scriban


var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();


var connStr = "Data Source=app.db";


// 初始化数据库(同前)

using (var initConn = new SqliteConnection(connStr))

{

    initConn.Open();

    var cmd = initConn.CreateCommand();

    cmd.CommandText =

    @"CREATE TABLE IF NOT EXISTS demo (

        id INTEGER PRIMARY KEY,

        msg TEXT

      );";

    cmd.ExecuteNonQuery();

}


// 注册模板文件路径

var templatePath = Path.Combine("Templates", "TimeTemplate.sbnhtml");


// GET /time

app.MapGet("/time", async () =>

{

    await using var conn = new SqliteConnection(connStr);

    await conn.OpenAsync();


    // 1. 取数据库时间

    var cmdTime = conn.CreateCommand();

    cmdTime.CommandText = "SELECT datetime('now')";

    var dbTime = (string)await cmdTime.ExecuteScalarAsync()!;


    // 2. 取 demo 表内容

    var cmdMsg = conn.CreateCommand();

    cmdMsg.CommandText = "SELECT msg FROM demo LIMIT 1";

    var msg = (await cmdMsg.ExecuteScalarAsync())?.ToString() ?? "(empty)";


    // 3. 用 Scriban 渲染模板

    var template = Template.Parse(await File.ReadAllTextAsync(templatePath));

    var html = await template.RenderAsync(new { db_time = dbTime, msg });


    return Results.Content(html, "text/html");

});


app.MapGet("/", () => "Visit /time to see the Scriban-powered page.");


app.Run();



Scriban:高效、强大的.NET开源模板引擎

点击查看原文

源码地址

简洁的语法:Scriban模板语言的设计目标是易读性和简洁性。例如,{{ variable }}用于输出变量,{% if condition %}...{% endif %}进行条件判断,{% for item in collection %}...{% endfor %}则用于循环操作。

1、简单使用


// 解析scriban 模板

var template = Template.Parse("Hello {{name}}!");


//结果:Hello World!

var result = template.Render(new { Name = "World" });


2、liquid模板

// 解析 liquid 模板

var template = Template.ParseLiquid("Hello {{name}}!");


//结果:Hello World!

var result = template.Render(new { Name = "World" });


3、循环生成文本

//循环模板

var template = Template.Parse(@"

<ul id='products'>

  {{ for product in products }}

    <li>

      <h2>{{ product.name }}</h2>

           Price: {{ product.price }}

           {{ product.description | string.truncate 15 }}

    </li>

  {{ end }}

</ul>

");

var result = template.Render(new { Products = this.ProductList });


{% if condition %}

  This will be rendered if condition is true.

{% else %}

  This will be rendered if condition is false.

{% endif %}


{% if condition1 %}

  Condition 1 is true.

{% else %}

  {% if condition2 %}

    Condition 2 is true.

  {% else %}

    None of the conditions are true.

  {% endif %}

{% endif %}


{% if condition1 && condition2 %}

  Both condition1 and condition2 are true.

{% endif %}


{% if condition1 or condition2 %}

  At least one of the conditions is true.

{% else %}

  Both conditions are false.

{% endif %}






Net Core 2022视频教程

点击查看原视频



.net6的obj->(Debug,Release)下的xx.GlobalUsings.g.cs引入全局空间避免每个cs文件都需要重复引用,该代码为编译时自动生成,代码如下

// <auto-generated/>

global using global::System;

global using global::System.Collections.Generic;

global using global::System.IO;

global using global::System.Linq;

global using global::System.Net.Http;

global using global::System.Threading;

global using global::System.Threading.Tasks;


你也可以自己建立个GlobalspaceName.cs,将全局的放入如

global using Test;


$格式化字符串


string name="xn";

int age=1;

$"第一个变量{name},第二个变量{age}"


=>这是.net3.5新出的lambda表达式,表示一个匿名函数,=>左边是参数,右边是函数体

delegate int Method(int a, int b);

Method m += (a ,b) => a + b;

Console.WriteLine(m(2, 3));



C# Action和Func的用法详解

委托是一个类,定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递。

   把一个 参数类型 返回值 相同 方法名不同 的方法当变量 的方法 叫委托。

   为了实现程序的六大设计中的开闭原则:解耦,对修改关闭,对扩展开放。逻辑分离。

   直接调用函数和使用委托调用函数的区别就是是否方便对外扩展。

   当我们窗体传值、线程启动时绑定方法、lambda表达式、异步等等情况下需要用到。

事件是一种特殊的委托,本质就是委托,事件是回调机制的一种应用

   当委托调用的函数达到某种条件时,可以通过回调通知调用者。


1. delegate 至少0个参数,至多32个参数,可以无返回值,可以指定返回值类型


private delegate int MethodDelegate(int x,int y); //两个参数,返回int类型

static void Main(string[]  args)

{

  MethodDelegate method = new MethodDelegate(Add);

  int result = method(10,20);  

}

private static int Add(int x, int y)

{

  return x +y;

}      


平时用委托的一般方式,先声明委托,然后再去使用,有点麻烦,.net中有已经定义好的委托类型Action 和 Func,可以拿来直接用。


2. Action  至少0个参数,至多16个参数,没有返回值的泛型委托


static void Main(string[] args)

{

    Action<string> BookAction = new Action<string>(Book); //一个参数

    BookAction("百年孤独");

}

public static void Book(string BookName)

{

    Console.WriteLine("我是买书的是:{0}",BookName);

}



using System;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class ActionDemo : MonoBehaviour

{

    Action  action;//表示无参

    Action<int> action1;//表示有传入参数int

    void Start()

    {

        action = actionH1;//没有参数

        action();


        action1 = actionH2;//一个 int参数 

        action1(456);


        actionH3(() => { Debug.Log("执行完actionH3了"); });//lambda 表达式 来执行委托


        actionH3(actionH4);//执行完 actionH3后回调 actionH4方法

    }


    private void actionH1()//没有参数

    {

        Debug.Log(123);

    }

    private void actionH2(int index)//参数int

    {

        Debug.Log(index);

    }

    private void actionH3(Action act)//参数 Action

    {

        Debug.Log("在执行actionH3");

        act();//回调 这个 委托方法

    }

    private void actionH4()//执行完 actionH3后的回调执行

    {

        Debug.Log("执行完actionH3了");

    }

}   


3. Func  至少0个参数,至多16个参数,必须有返回值的泛型委托


没有参数只有返回值的方法

static void Main(string[] args)

{

    Func<string> RetBook = new Func<string>(FuncBook); //无参的Func 返回一个string

    //Func<string> RetBook = FuncBook; //也可以直接这样

    Console.WriteLine(RetBook); //执行 这个委托

}

public static string FuncBook()

{

    return "送书来了";

}


有参数有返回值的方法


static void Main(string[] args)

{

    Func<string, string> RetBook = new Func<string, string>(FuncBook); //有一个参数的Func,返回一个string

    //Func<string,string> RetBook = FuncBook; //也可以直接这样

    Console.WriteLine(RetBook("aaa")); //执行 这个委托

}

public static string FuncBook(string BookName)

{

    return BookName;

}


Func一个很重要的用处就是传递值,下面举一个简单的代码来说明


Func<string> funcValue = delegate

{

    return "我是即将传递的值3";

};

DisPlayValue(funcValue);

注释1:DisplayVaue是处理传来的值,比喻缓存的处理,或者统一添加数据库等


private static void DisPlayValue(Func<string> func)

{

    string RetFunc = func();

    Console.WriteLine("我在测试一下传过来值:{0}",RetFunc);

}

完整示列如下:


public class Person 

      { 

         public string Name { set; get; } 

         public int Age { set; get; } 

         public bool Gender { set; get; } 

         /// <summary> 

         /// 重写tostring方法,方便输出结果 

         /// </summary> 

         /// <returns></returns>

         public override string ToString()

         {

             return Name + "\t" + Age + "\t" + Gender;

         }

     }

     class Program

     {

         static void Main(string[] args)

         {

             Func<Person, Person> funcUpdateAge = new Func<Person, Person>(UpdateAge);

             Func<Person, Person> funcUpdateAge2 = UpdateAge; 

             Func<Person, Person> funcUpdateGender = (p1) => { p1.Gender = false; return p1; };//lambda表达式方式

             Func<Person, Person> funUpdateName = delegate(Person p2)//匿名方法

             {

                 p2.Name = "Wolfy2";

                 return p2;

             };

             Person p = new Person() { Name = "Wolfy", Age = 24, Gender = true };

             Person result = funcUpdateAge(p);

             Person result2 = funcUpdateAge2(p);

             Console.WriteLine(result.ToString());

             Console.WriteLine(result2.ToString());

             Console.WriteLine(funcUpdateGender(p).ToString());

             Console.WriteLine(funUpdateName(p).ToString());

             Console.Read();

         }

         static Person UpdateAge(Person p)

         {

             p.Age = 25;

             return p;

         }

     }


记住无返回值就用Action,有返回值就用Func

Action:无参数无返回值委托。

Action<T>:泛型委托,无返回值,根据输入参数的个数不同有十六个重载。

Func<out T>:无输入参数,有返回值。

Func<in T,out T>:有输入参数,有返回值,根据输入参数个数不同,有十六个重载。

Action和Func中可以使用Lambda和匿名方法处理方法体内逻辑。





Func是一种委托,这是在3.5里面新增的,2.0里面我们使用委托是用Delegate,Func位于System.Core命名空间下,使用委托可以提升效率,例如在反射中使用就可以弥补反射所损失的性能。


Action<T>和Func<T,TResult>的功能是一样的,只是Action<T>没有返类型,


Func<T,T,Result>:有参数,有返回类型

Action,则既没有返回也没有参数,



Func<T,TResult> 

的表现形式分为以下几种:


1。Func<T,TResult>

2。Func<T,T1,TResult>

3。Func<T,T1,T2,TResult>

4。Func<T,T1,T2,T3,TResult>

5。Func<T,T1,T2,T3,T4,TResult>


分别说一下各个参数的意义,TResult表示 

委托所返回值 所代表的类型, T,T1,T2,T3,T4表示委托所调用的方法的参数类型,


以下是使用示例:


Func<int, bool> myFunc = null;//全部变量


myFunc = x => CheckIsInt32(x); 

//给委托封装方法的地方 使用了Lambda表达式


private bool CheckIsInt32(int pars)//被封装的方法

{

  return pars == 5;

}


  bool ok = myFunc(5);//调用委托


MSDN:http://msdn.microsoft.com/zh-cn/library/bb534303(VS.95).aspx




但是如果我们需要所封装的方法不返回值,增么办呢?就使用Action!


可以使用 

Action<T1, T2, T3, T4>委托以参数形式传递方法,而不用显式声明自定义的委托。封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法必须具有四个均通过值传递给它的参数,并且不能返回值。(在 C# 中,该方法必须返回 void。在 Visual Basic 中,必须通过 Sub…End Sub 结构来定义它。)通常,这种方法用于执行某个操作。


使用方法和Func类似!


MSDN:http://msdn.microsoft.com/zh-cn/library/bb548654(VS.95).aspx



Action:既没有返回,也没有参数,使用方式如下:


Action 

action = null;//定义action


action =  CheckIsVoid;//封装方法,只需要方法的名字


action();//调用




总结:


使用Func<T,TResult>和Action<T>,Action而不使用Delegate其实都是为了简化代码,使用更少的代码达到相同的效果,不需要我们显示的声明一个委托。


Func<T,TResult>的最后一个参数始终是返回类型,而Action<T>是没有返回类型的,而Action是没有返回类型和参数输入的。




MVC开发笔记

点击查看原视频

使用session给视传递参数各种方式

4.png

5.png4.png


5.png

Linux上部署ASP.NET Core

CentOS安装ASP.NET core运行环境

可以安装文件传输软件便于传输文件MobaXterm

yum update

yum insytall net-tools

注册镜像地址

sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm


安装ASP.NET Core SDK

sudo yum update

sudo yum install donet-sdk-6.0


关闭防火墙

systemctl disable firewalld


VS20022发布运行到Linux上的asp.net core,要改项目的launchSetting.json里的

项目URL地址

4.png


启动

用MobaXTerm上传到linux

cd /root/NET6Publish/

ls

clear

dotnet Advanced.NET6.Project.dll --urls=http://192.168.3.91:5999


无值守启动

nohup dotnet Advanced.NET6.Project.dll --urls=http://192.168.3.91:5999 &


让进程不停止  

dotnet Advanced.NET6.Project.dll --urls=http://192.168.3.91:5999  &


面向切面编程AOP

Aspect Oriented Programming在不修改之前的代码为基础,可以动态的增加业务逻辑

如果可以再已经成型的程序众,如果可以动态的在一些行为之前增加点内容,在一些行为之后增加点内容--之前已经开发好的内容保持不变



在ASP.NET Core中使用Redis作为Session

前言

Session 是保存用户和 Web 应用的会话状态的一种方法,ASP.NET Core 提供了一个用于管理会话状态的中间件。在本文中我将会简单介绍一下 ASP.NET Core 将Redis作为 Session 的使用方法。

要将Redis作为Session的前提条件是安装并启用了Session,并在Nuget中安装以下两个包:

Microsoft.AspNetCore.Session

Microsoft.Extensions.Caching.Redis

Windows版Redis下载地址:https://github.com/MSOpenTech/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.msi

使用

需要注意的一点是,要使用Session,必须将

services.Configure<CookiePolicyOptions>(options =>

{

  options.CheckConsentNeeded = context => true;

  options.MinimumSameSitePolicy = SameSiteMode.None;

});

中的options.CheckConsentNeeded = context => true;修改为options.CheckConsentNeeded = context => false;true表示用户是否同意cookie协议。当为true时而又无弹窗提示用户是否同意时,将导致session无法回传,故设置为false才可正常使用session。因为SessionID是存放在用户浏览器Cookie当中的,如果不回传SessionId,肯定就没法获取当前设置的Session。


1、Redis连接字符串


在appsettings.json中添加Redis连接字符串。


"ConnectionStrings": {

    "RedisSessionConnectionString": "127.0.0.1:6379,password=easy_net_770702827,ConnectTimeout=3000,defaultdatabase=1"

  },


2、配置

在Startup.cs中的 ConfigureServices方法下添加:


//获取连接字符串

var sessionConnectionString = Configuration.GetConnectionString("RedisSessionConnectionString");

//使用Session

services.AddSession(options =>

{

    options.IdleTimeout = TimeSpan.FromMinutes(30);

    options.Cookie.HttpOnly = true;

});

//使用Redis作为Session缓存

services.AddDistributedRedisCache(options => 

{

    options.Configuration = sessionConnectionString;

    options.InstanceName = "HFMSCache";

});

此时 ConfigureServices 方法如下所示:


public void ConfigureServices(IServiceCollection services)

{

    var sessionConnectionString = Configuration.GetConnectionString("RedisSessionConnectionString");

    services.Configure<CookiePolicyOptions>(options =>

    {

        // This lambda determines whether user consent for non-essential cookies is needed for a given request.

        options.CheckConsentNeeded = context => true;

        options.MinimumSameSitePolicy = SameSiteMode.None;

    });


    //使用Session

    services.AddSession(options =>

    {

        options.IdleTimeout = TimeSpan.FromMinutes(30);

        options.Cookie.HttpOnly = true;

    });

    //使用Redis作为Session缓存

    services.AddDistributedRedisCache(options =>

    {

        options.Configuration = sessionConnectionString;

        options.InstanceName = "HFMSCache";

    });


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

}

3、启用Session


在Configure方法中添加:


app.UseSession();

注意:UseSession必须在UseMvc之前!


此时 Configure 方法如下所示:


public void Configure(IApplicationBuilder app, IHostingEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

    }

    else

    {

        app.UseExceptionHandler("/Home/Error");

    }


    app.UseStaticFiles();

    app.UseCookiePolicy();


    app.UseSession();


    app.UseMvc(routes =>

    {

        routes.MapRoute(

            name: "default",

            template: "{controller=Home}/{action=Index}/{id?}");

    });

}

4、测试


在HomeController下面修改方法Index和添加方法Set。分别是设置Session和获取Session,这里就不再赘述,直接列出代码。


public IActionResult Index()

{

    string user = HttpContext.Session.GetString("USER");

    if (string.IsNullOrEmpty(user))

    {

        ViewBag.Value = "Get user data failed.";

    }

    else

    {

        ViewBag.Value = user;

    }

    return View();

}


public IActionResult Set()

{

    HttpContext.Session.SetString("USER", "https://www.quarkbook.com/");

    return Content("Set value successful.");

}








Top