0%

Transact-SQL(又称T-SQL),是在Microsoft SQL Server和Sybase SQL Server上的ANSI SQL实现,与Oracle的PL/SQL性质相近(不只是实现ANSI SQL,也为自身数据库系统的特性提供实现支持),当前在Microsoft SQL Server和Sybase Adaptive Server中仍然被使用为核心的查询语言。

下文涉及的函数/方法限T-SQL使用,在MySQL和Oracle中未必兼容

存储过程的查询条件

待改进的一种条件拼接:

1
2
3
SELECT * FROM LocalExport where 
SSO=IIF(@SSO is null, SSO, @SSO)
and SN=IIF(@SN is null, SN, @SN)

T-SQL方法
IIF(expression, return value when ture, return value when false)

待改进是因为存在下述bug:当缺省SN过滤条件(即@SN为null)时,记录中SN列的值为空的行不会查出,即null=null为false

可以这么表达
1
2
3
SELECT * from LocalExport where 
(ISNULL(@SSO, '')='' OR SSO=@SSO)
and (ISNULL(@SN, '')='' OR SN=@SN)

动态SQL语句
1
2
3
4
5
6
7
8
9
10
SET @SQL='select * from LocalExport where 1=1';
IF @SSO is not null
BEGIN
SET @SQL=@SQL+' AND SSO=@SSO'
END
IF @SN is not null
BEGIN
SET @SQL=@SQL+' AND SN=@SN'
END
EXEC sp_executesql @SQL

国际化和本地化

Internationalization is the process of designing and preparing your app to be usable in different languages. Localization is the process of translating your internationalized app into specific languages for particular locales.

国际化是将应用程序设计以及预备,使之支持不同的语言的过程。 本地化是一个把国际化的应用根据区域配置翻译成特定语言的过程。

国际化设计是实现“多语言切换”的前提,angular框架的国际化基础是i18n模块

框架本地化(Localization)

为应用配置“地区”,这个地区成为查找相应的本地化数据的依据。

1
ng add @angular/localize

异常:

Uncaught Error: It looks like your application or one of its dependencies is using i18n.
Angular 9 introduced a global `$localize()` function that needs to be loaded.
Please run `ng add @angular/localize` from the Angular CLI.
(For non-CLI projects, add `import ‘@angular/localize/init’;` to your `polyfills.ts` file.

如上所述,需要添加 import ‘@angular/localize/init’ 到 polyfills.ts

模板翻译

  1. 在组件模板中标记需要翻译的静态文本信息。

    1
    2
    <h1 i18n>Hello QQs</h1>
    <h1 i18n="say hello|translate hello">Hello QQs</h1>

    在组件上添加i18n属性,标记该文本待翻译,另外可附上“<意图>|<描述>”,注意这些文字并不作为翻译条目的标识符,只是增强代码的可读性

    i18n提取工具会为这些标记的单元生成一个随机的id,这个才是标识符,该id可自定义如下

    1
    <h1 i18n="@@introductionHeader">Hello QQs</h1>

    非文本内容标签(待翻译文本是组件属性,而非内容)的标记

    1
    <img [src]="logo" i18n-title title="Angular logo" />

    单复数问题

    1
    2
    3
    4
    5
    <span i18n>Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}</span>
    <!-- 期望: -->
    <!-- 当minutes=0时,翻译文本“Updated just now” -->
    <!-- 当minutes=1时,翻译文本“Updated one minute ago” -->
    <!-- 其他,翻译文本“Updated xx minutes ago” -->

    plural /ˈplʊərəl/ 复数的 这里是复数翻译模式的关键字

    选择问题

    1
    <span i18n>The author is {gender, select, male {male} female {female} other {other}}</span>
  2. 创建翻译文件:使用 Angular CLI 的 xi18n 命令,把标记过的文本提取到一个符合行业标准的翻译源文件中。
    1
    ng xi18n --i18n-format=xlf --output-path src/locale --out-file translate.xlf
    得到的是一个xml语言的标准格式文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <?xml version="1.0" encoding="UTF-8" ?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="ng2.template">
    <body>
    <trans-unit id="introductionHeader" datatype="html">
    <source>Hello QQs</source>
    <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">353</context>
    </context-group>
    </trans-unit>
    <trans-unit id="updatetime" datatype="html">
    <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
    <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">354</context>
    </context-group>
    </trans-unit>
    <trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" datatype="html">
    <source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
    <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">354</context>
    </context-group>
    </trans-unit>
    </body>
    </file>
    </xliff>
  3. 编辑所生成的翻译文件:把提取出的文本翻译成目标语言。
    复制上面生成的翻译格式文件,填入翻译后的文本

    1
    2
    3
    4
    5
    6
    7
    8
    <trans-unit id="introductionHeader" datatype="html">
    <source>Hello QQs</source>
    <target>你好,爸爸</target>
    </trans-unit>
    <trans-unit id="updatetime" datatype="html">
    <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
    <target>{VAR_PLURAL, plural, =0 {方才} =1 {il y 刚片刻} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> 分钟前} }</target>
    </trans-unit>
  4. 将目标语言环境和语言翻译添加到应用程序的配置中。

    a. 配置AOT编译

    警告,此处存在版本差异:

    Angular v9 angular.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    "projects": {
    ...
    "my-project": {
    ...
    "i18n": {
    "sourceLocale": "en-US",
    "locales": {
    "fr": "src/locale/messages.fr.xlf",
    "zh": "src/locale/translate.zh-cn.xlf"
    }
    }
    }
    }

    调用ng build —prod —localize构建 i18n 下定义的所有语言环境。生成若干套静态资源:

    1
    2
    3
    4
    5
    dist
    └───my-project
    ├───en-US
    ├───fr
    └───zh

    配置特定的语言环境,指定Locale_ID

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    "build": {
    "configurations": {
    "fr": {
    "aot": true,
    "outputPath": "dist/my-project-fr/",
    "i18nFile": "src/locale/messages.fr.xlf",
    "i18nFormat": "xlf",
    "i18nLocale": "fr",
    "i18nMissingTranslation": "error" //报告缺失的翻译 提醒级别为error
    }
    ...
    }
    ...
    }

    Angular v8 angular.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    ...
    "architect": {
    "build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": { ... },
    "configurations": {
    "fr": {
    "aot": true,
    "outputPath": "dist/my-project-fr/",
    "i18nFile": "src/locale/messages.fr.xlf",
    "i18nFormat": "xlf",
    "i18nLocale": "fr",
    "i18nMissingTranslation": "error",
    }
    }
    },
    ...
    "serve": {
    "builder": "@angular-devkit/build-angular:dev-server",
    "options": {
    "browserTarget": "my-project:build"
    },
    "configurations": {
    "production": {
    "browserTarget": "my-project:build:production"
    },
    "fr": {
    "browserTarget": "my-project:build:fr"
    }
    }
    }
    }

    可见 angular.json中,为ng build命令添加了3个用于国际化的参数i18nFile,i18nFormat,i18nLocale。另外指定outputPath用以区分不同语言版本, 这些参数也可以通过cli附加命令的配置项

    1
    ng build --prod --i18n-file src/locale/messages.fr.xlf --i18n-format xlf --i18n-locale fr

    ng serve 命令是通过browsertarget:my-project:build使用ng build 的配置,并不直接支持上述用于国际化的参数,而以下面的方式调用

    1
    ng serve --configuration=fr

    b. 配置JIT编译

    • 载入合适的翻译文件,作为字符串常量
    • 创建相应的translation provider
    • 使用translation provider启动app

    main.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import { enableProdMode, TRANSLATIONS, TRANSLATIONS_FORMAT, MissingTranslationStrategy } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';

    if (environment.production) {
    enableProdMode();
    }

    // use the require method provided by webpack
    declare const require;
    // we use the webpack raw-loader to return the content as a string
    const translations = require('raw-loader!./locale/translate.fr.xlf').default;

    platformBrowserDynamic().bootstrapModule(AppModule,{
    missingTranslation: MissingTranslationStrategy.Error,
    providers: [
    {provide: TRANSLATIONS, useValue: translations},
    {provide: TRANSLATIONS_FORMAT, useValue: 'xlf'}
    ]
    })
    .catch(err => console.error(err));

    app.module.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @NgModule({
    declarations: [
    AppComponent
    ],
    imports: [
    BrowserModule,
    AppRoutingModule
    ],
    providers: [{ provide: LOCALE_ID, useValue: 'fr' }],
    bootstrap: [AppComponent]
    })
    export class AppModule { }

多语言切换 ngx-translate

关于ngx-translate Ngx-translate文章

关于认证

参考ASP.NET Core 中的那些认证中间件及一些重要知识点

ASP.NET Core 中,身份验证由 IAuthenticationService 负责,而它供身份验证中间件使用。

身份验证中间件

已注册的身份验证处理程序及其配置选项被称为“方案(schema)”。

Authentication schemes are specified by registering authentication services in Startup.ConfigureServices:
在startup.cs的ConfigureServices中通过注册身份认证,指定认证方案

1
2
3
4
5
public void ConfigureServices(IServiceCollection services){
services.AddAuthentication("YourSchemaName")
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options))
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options));
}

AddAuthentication的参数是方案名称,默认值为JwtBearerDefaults.AuthenticationScheme(即”Bearer”)。

可使用多种身份验证方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void ConfigureServices(IServiceCollection services)
{
// 认证方案

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
})
.AddJwtBearer("AzureAD", options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://login.microsoftonline.com/eb971100-6f99-4bdc-8611-1bc8edd7f436/";
});

// 授权访问
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"AzureAD");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
}

再说方案名称(AuthenticationScheme),可使用方案名称来指定应使用哪种(或哪些)身份验证方案来对用户进行身份验证。 当配置身份验证时,通常是指定默认身份验证方案。除非资源请求了特定方案,否则使用默认方案。

授权策略(authorization policy)
下文中有使用特性注解为资源(如api)指定授权方案的栗子
自定义策略提供程序-IAuthorizationPolicyProvider

文章Asp.Net Basic Authentication自定义了使用Basic Auth进行认证的方案,配置方案如

1
2
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

BasicAuthenticationHandler是自定义的身份验证处理类,派生自AuthenticationHandler\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IUserService _userService;

public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IUserService userService)
: base(options, logger, encoder, clock)
{
_userService = userService;
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");

User user = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
user = await _userService.Authenticate(username, password);
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}

if (user == null)
return AuthenticateResult.Fail("Invalid Username or Password");

var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);

return AuthenticateResult.Success(ticket);
}
}

类型定义身份验证操作,负责根据请求上下文构造用户的身份。 返回一个 AuthenticateResult指示身份验证是否成功

选择具有策略的方案

1
2
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options))
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options));

ASP.NET Core 中的授权通过 AuthorizeAttribute 和其各种参数来控制。 在最简单的形式中,将 [Authorize] 属性应用于控制器、操作或 Razor 页面,将对该组件的访问限制为任何经过身份验证的用户。

1
2
3
4
5
6
7
8
9
10
namespace QQsServices.Controllers
{
[Authorize]
[Route("api/v1/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
......
}
}

允许未通过验证的访问—AllowAnonymous
1
2
3
4
5
6
7
8
9
10
11
12
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}

public ActionResult Logout()
{
}
}

Authorize从controller到action向下继承,而AllowAnonymous覆盖Authorize(AllowAnonymous优先级高于Authorize)

JwtBearer

持有者身份验证

Basic Auth

Cors

基于策略的授权

startup.cs

1
2
3
4
5
6
7
8
9
10
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.AddPolicy("PolicyBased01", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
}

指定特定授权方案

参考 Authorize with a specific scheme in ASP.NET Core

An authentication scheme is named when the authentication service is configured during authentication. Startup的服务配置中, AddAuthentication后添加方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void ConfigureServices(IServiceCollection services)
{
// Code omitted for brevity

services.AddAuthentication()
.AddCookie(options => {
options.LoginPath = "/Account/Unauthorized/";
options.AccessDeniedPath = "/Account/Forbidden/";
})
.AddJwtBearer(options => {
options.Audience = "http://localhost:5001/";
options.Authority = "http://localhost:5000/";
});
....
}

使用授权属性选择方案
1
2
3
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class MixedController : Controller
.....

使用策略并指定授权方案
If you prefer to specify the desired schemes in policy, you can set the AuthenticationSchemes collection when adding your policy. 添加授权策略时设置AuthenticationSchemes列表,将对应的scheme添加进去
1
2
3
4
5
6
7
8
9
services.AddAuthorization(options =>
{
options.AddPolicy("Over18", policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new MinimumAgeRequirement());
});
});

在属性中使用指定策略
1
2
[Authorize(Policy = "Over18")]
public class RegistrationController : Controller

使用多种方案

the following code in Startup.ConfigureServices adds two JWT bearer authentication schemes with different issuers:颁发者不同的两种JWT Bearer认证方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ConfigureServices(IServiceCollection services)
{
// Code omitted for brevity

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://localhost:5000/identity/";
})
.AddJwtBearer("AzureAD", options =>
{
options.Audience = "https://localhost:5000/";
options.Authority = "https://login.microsoftonline.com/eb971100-6f99-4bdc-8611-1bc8edd7f436/";
});
...
}

更新授权策略, 上面两种JWTBearer认证方案,将一个要注册到默认认证方案“JwtBearerDefaults.AuthenticationScheme”,另外的认证方案需要以唯一的方案注册(Only one JWT bearer authentication is registered with the default authentication scheme JwtBearerDefaults.AuthenticationScheme. Additional authentication has to be registered with a unique authentication scheme.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void ConfigureServices(IServiceCollection services)
{
// Code omitted for brevity

services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
JwtBearerDefaults.AuthenticationScheme,
"AzureAD");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
}

参考在 ASP.NET Core 中使用 IAuthorizationPolicyProvider 的自定义授权策略提供程序

.net core

.NET Core 是一个通用的开放源代码开发平台。 可以使用多种编程语言针对 x64、x86、ARM32 和 ARM64 处理器创建适用于 Windows、macOS 和 Linux 的 .NET Core 应用。 为云、IoT、客户端 UI 和机器学习提供了框架和 API。

  • 运行时和SDK
    运行 .NET Core 应用,需安装 .NET Core 运行时。
    创建 .NET Core 应用,需安装 .NET Core SDK。
  • 命令行工具
    在命令行键入dotnet —help 文章ASP和SPA有所应用。

    另,代码生成器(codesmith generator studio) 和Nhibernate Template 根据数据库表生成实体类及MVC分层结构

  • NuGet
    包管理工具,用于安装依赖NuGet包或用于安装模板

asp.net core

经典的 .net mvc 分层:

view controller model

asp.net (asp, Active Server Pages 动态服务器页面):

路由

模型绑定
模型验证
依赖关系注入
筛选器
Areas
Web API
Testability
Razor查看引擎
强类型视图
标记帮助程序
查看组件

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

Host(宿主)为app提供运行环境并负责启动,创建HostBuilder,指定Startup作为启动类

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<DataServiceContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DataServiceContext")));
}

public void Configure(IApplicationBuilder app)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc();
}
}

startup.cs中using所需地package,包括需要加载(use)地中间件。

另,这里引入了SQL Server需要安装并引入 Microsoft.EntityFrameworkCore;
Microsoft.EntityFrameworkCore.SqlServer;

launchSettings.json

依赖注入Iconfigure,读取配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OidcConfigurationController : ControllerBase
{
private IConfiguration _configuration;
public OidcConfigurationController(IConfiguration Configuration)
{
_configuration = Configuration;
}

[HttpGet]
[Route("snmapi/[controller]/auth")]
public CSResult GetOidcConfigure()
{
var authSettings = new JObject{
{ "client_id", _configuration["Auth:client_id"] },
{ "redirect_uri", _configuration["Auth:redirect_uri"] },
{ "post_logout_redirect_uri", _configuration["Auth:post_logout_redirect_uri"] },
};
}
}

只能逐个value地取值,其他读取方法见ASP.Net Core读取配置文件的三种方法

Properties/launchSettings.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:8080",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"RestrictedProductMaintenance": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

这个配置似乎都是关于运行asp的IIS的。

创建Model

issue: Unable to cast object of type ‘System.Guid’ to type ‘System.String’.

SQL Server 的 uniqueidentifier类型列,在 .net core 中直接映射为Guid类型,不存在与string的隐式转换,因此对应Model里的字段应该是Guid,在必要的场合使用toString

插入Guid

1
Guid id = Guid.NewGuid();

Model定义中的数据库特性

从数据库生成模型

1
Scaffold-DbContext  "Data Source=HostName; Initial Catalog=DBName; Persist Security Info=True; User ID=UserName; Password=******;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

DBContext

1
2
3
4
5
6
7
8
9
public class DataServiceContext: DbContext
{
public DataServiceContext(DbContextOptions<DataServiceContext> options) : base(options)
{

}

public DbSet<RestrictedProductDTO> RestrictedProduct { get; set; }
}

在Startup.cs中添加

1
2
3
4
5
6
 public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DataContext")));
}

ConnectionString是从appsettings.json中读取的
1
2
3
4
"connectionStrings": {
"DataContext": "Server=sqlserver01;Database=db01;Trusted_Connection=False;user id=user01;password=xxxx;"
},
...

数据上下文访问SQL Server,会提示安装Microsoft.EntityFrameworkCore

这个数据库上下文将被注入controlloer或model中用以获取数据

controller

使用Add—Add New Scaffold Item(基架项)选择 MVC controlloer with views, using Entity Framework

之后在对话框中配置业务需要的Model、Data context等,此举一并生成controller views dbcontext代码,views包含index,create,detail,edit,delete,视图上的请求会自动在controller中实现接口

Error There was an error running the selected code generator:”No parameterless constructor defined for type ‘XXController’”

曾现此错误,是因为待注入的Data context未配置正确(在startup中添加configureService,其中的connectionstring配置在appsettings.json中)

创建完成的基架controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public class ProductsController : Controller
{
private readonly DataContext _context;

public ProductsController(DataContext context)
{
_context = context;
}

// GET: Products
public async Task<IActionResult> Index()
{
return View(await _context.ProductData.ToListAsync());
}

// GET: Products/Details/5
public async Task<IActionResult> Details(string id)
{
if (id == null)
{
return NotFound();
}

var product = await _context.ProductData
.FirstOrDefaultAsync(m => m.Id == id);
if (product == null)
{
return NotFound();
}

return View(product);
}

// GET: Products/Create
public IActionResult Create()
{
return View();
}

// POST: Products/Create
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,SN,Brand,CreateTime,ProductName,Restriction")] Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(product);
}

// GET: Products/Edit/5
public async Task<IActionResult> Edit(string id)
{
if (id == null)
{
return NotFound();
}

var product = await _context.ProductData.FindAsync(id);
if (product == null)
{
return NotFound();
}
return View(product);
}

// POST: Products/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Id,SN,Brand,CreateTime,ProductName,Restriction")] Product product)
{
if (id != product.Id)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(product);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(product.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(product);
}

// GET: Products/Delete/5
public async Task<IActionResult> Delete(string id)
{
if (id == null)
{
return NotFound();
}

var product = await _context.ProductData
.FirstOrDefaultAsync(m => m.Id == id);
if (product == null)
{
return NotFound();
}

return View(product);
}

// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id)
{
var product = await _context.ProductData.FindAsync(id);
_context.ProductData.Remove(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

private bool ProductExists(string id)
{
return _context.ProductData.Any(e => e.Id == id);
}
}

return View() 返回视图,路由是根据Controller名称和方法名称组成的,如这里的Product/Index,Product/Create

Controller vs ControllerBase

如果你创建一个 .net core api项目,你会发现api的controller继承ControllerBase,而asp继承Controller

Controller是ControllerBase的衍生类,支持asp的Views。

Views

创建Product/Index.cshtml, 这是一个列表页。顶头:
@model IEnumerable\

@model 指令使你能够使用强类型的 Model 对象访问控制器传递给视图的列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对列表项进行循环遍历 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<table class="table">
<thead>
<tr>
<th>SN</th>
<th>Brand</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>Id
<td>@Html.DisplayFor(modelItem => item.SN)</td>
<td>@Html.DisplayFor(modelItem => item.Brand)</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>

实际上,dotnet提供了视图基架Add—Add New Scaffold Item(基架项)选择 MVC View之后在对话框中配置业务需要的Model、Data context等,选择模板(list, details, create, edit, delete)

判断是否为null
1
2
3
4
5
6
7
8
@foreach (var item in Model.childrenMap??new List<Child>())
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Child.Name)
</td>
</tr>
}

模型驱动数据库设计

因需求变动,模型结构变化,与数据库产生冲突

Code First迁移数据模型

创建数据模型Model,在Nuget Package Manager —> Package Manager Console中执行

1
2
3
Add-Migration updateModelStructure
// updateModelStructure 是一个数据库操作的标题,是任意命名的,仅用于记录更新记录和历史
Update-Database

Add-Migration之后项目中生成了Migrations类,这些类阐释了如何修改数据库见 《Entity Framework》篇

issue: The entity type ‘XXModel’ requires a primary key to be defined.

Model类型需标注主键
1
2
3
4
5
6
private Guid _id;
[Key]
public Guid ID
{
get { return _id; }
}

issue “No context type was found in the assembly ‘MyServices.Models’.”
在多项目解决方案中,执行Migration的是Models项目,读取Startup中的context配置, 检查Startup.cs的addDbContext配置
另有命令
1
Enable-Migrations -ProjectName MyServices.Models -StartUpProjectName MyServices.API -ContextTypeName MyServices.Models.Contexts.DataContext -Verbose 

issue “The type ‘xxContext’ does not inherit from DbContext. The DbMigrationsConfiguration.ContextType property must be set to a type that inherits from DbContext.”
解决方法是在startup项目安装 Microsoft.EntityFrameworkCore.Tools

业务API

前端组件 Blazor

发布到IIS

TroubleShooting

IIS Issue: HTTP错误500.19

微软.net core 下载页面有如下加粗提示:

On Windows, we recommended installing the Hosting Bundle, which includes the .NET Core Runtime and IIS support.

下载后得到dotnet-hosting-3.0.x.exe
另:IIS的远程访问允许
新建防火墙规则

另: IIS 路由站点
新建IIS 的网站指向一个物理路径就可以了,右击网站添加应用程序,指向发布的文件目录,并添加别名,事实上这个别名会作为虚拟路径(子路由),实现192.168.1.100:80/App1这样的部署。

IIS Issue: Chrome 400 Bad Request

某次更新服务后,在IIS上Browser website测试正常,在远程chrome上访问只能get post全部400,新打开一个ie发现是好的(实际上新打开一个chrome隐私窗口也会是好的,这个问题初步被认为与浏览器写了太多cookie有关)

配置多个环境

https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/environments?view=aspnetcore-3.1

solution和projects

多项目解决方案(class library projects)

关于多个项目启动

关于[HttpGet][HttpPost]标注以及方法HttpMethod识别

CSDN Blog WebApi进阶

博主的理解是:方法名以Get开头,WebApi会自动默认这个请求就是get请求,而如果你以其他名称开头而又不标注方法的请求方式,那么这个时候服务器虽然找到了这个方法,但是由于请求方式不确定,所以直接返回给你405——方法不被允许的错误。
结论:所有的WebApi方法最好是加上请求的方式([HttpGet]/[HttpPost]/[HttpPut]/[HttpDelete]),不要偷懒,这样既能防止类似的错误,也有利于方法的维护,别人一看就知道这个方法是什么请求

TargetFramework

项目属性中的item,如\net6-windows10.0.22621.0\ Visual Studio解析为

  • Target framework: .Net 6.0
  • Target OS: Windows
  • Target OS version: 10.0.22621.0

一般只需要.Net6即可,在 OS 特定的 TargetFramework 的末尾指定可选的 OS 版本,例如,net6.0-ios15.0。 版本指示应用或库可用的 API。 它不控制应用或库在运行时支持的 OS 版本。 它用于选择项目编译的引用程序集,并用于从 NuGet 包中选择资产。 将此版本视为“平台版本”或“OS API 版本”,可以与运行时 OS 版本进行区分。

当特定于 OS 的 TargetFramework 不显式指定平台版本时,它具有可从基础 TargetFramework 和平台名称推断的隐含值。 例如,.NET 6 中 iOS 的默认平台值为 15.0,这意味着 net6.0-ios 是规范 net6.0-ios15.0 TargetFramework 的简写形式。

安装配置webpack

1
npm i webpack webpack-cli

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const path = require('path')

module.exports = {
mode:'development',
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
}
}

1
npx webpack --config webpack.config.js

模块

模块化编程中,开发者将程序分解为功能离散的chunk,并称之为模块

Node.js是从一开始就支持模块化编程的环境,其将一段js逻辑export为一个module,随着web发展,模块化逐渐得到支持,webpack工具设计为可将模块化应用到任何文件中
除了js的module外,还有css/less/sass的module,以及wasm的module等

不同‘文件和语言的’模块使用对应的loader引入app
demo project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- data.xml
|- my-font.woff
|- my-font.woff2
|- icon.png
|- style.css
|- index.js
|- /node_modules

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
},
{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},
{
test: /\.xml$/,
use: [
'xml-loader'
]
}
]
}
};

index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import _ from 'lodash';
import './style.css';
import Icon from './icon.png';
import Data from './data.xml';

function component() {
var element = document.createElement('div');

// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');

// Add the image to our existing div.
var myIcon = new Image();
myIcon.src = Icon;

element.appendChild(myIcon);

console.log(Data);

return element;
}

document.body.appendChild(component());

管理输出

webpack 将打包好的各模块的bundle.js文件引用到app入口index.js中,若对入口文件修改可如下配置,使bundle.js引用到新的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');

module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
plugins: [new HtmlWebpackPlugin()]
}
};

source map devtool plugin

源码映射以便随浏览器运行进行调试,参考 SourceMapDevToolPlugin

webpack-bundle-analyzer

模块打包结构分析插件,参考webpack官方—webpack-bundle-analyzer

安装

1
npm intall --save-dev webpack-bundle-analyzer

在Angular项目中使用
1
2
ng build --prod --stats-json
npx webpack-bundle-analyzer dist/stats.json

在自定义项目中, 添加plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.config.js 文件

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports={
plugins: [
new BundleAnalyzerPlugin() // 使用默认配置
// 默认配置的具体配置项
// new BundleAnalyzerPlugin({
// analyzerMode: 'server',
// analyzerHost: '127.0.0.1',
// analyzerPort: '8888',
// reportFilename: 'report.html',
// defaultSizes: 'parsed',
// openAnalyzer: true,
// generateStatsFile: false,
// statsFilename: 'stats.json',
// statsOptions: null,
// excludeAssets: null,
// logLevel: info
// })
]
}

link-app-bundle-analyze
scan-link, react+vtk.js+materialUI的项目

loader

loader是webpack可调用的一些对文件预处理的模块
内联调用loader

1
import MyIcon from '-!svg-react-loader!../../assets/image/icon.svg'

typescript

webpack 集成 typescript:

1
npm install --save-dev typescript ts-loader

添加tsconfig.json
1
2
3
4
5
6
7
8
9
10
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true
}
}

环境变量

There is also a built-in environment variable called NODE_ENV. You can read it from process.env.NODE_ENV. When you run npm start, it is always equal to ‘development’, when you run npm test it is always equal to ‘test’, and when you run npm run build to make a production bundle, it is always equal to ‘production’. You cannot override NODE_ENV manually. This prevents developers from accidentally deploying a slow development build to production. ————《create-react-app自定义环境变量》

禁止生成SourceMap, 注意不要在&&之前添加多余空格

1
set GENERATE_SOURCEMAP=false&& yarn build

npm ls

npm prune 清理无关package

npm i —prefix

issue: npm ERR! Error: EPERM: operation not permitted, rename

use ‘npm cache clean’
npm install fails on Windows: “Error: EPERM: operation not permitted, rename” #10826

疑杀毒软件问题

npx

npx是npm的命令,创建React App时使用了如下命令

1
npx create-react-app my-app

npm 用于包管理(安装、卸载、调用已安装的包blabla),npx在此基础上提高使用包的体验,实际上,调用上述命令时,npm依次查找create-react-app的依赖,无法找到则从网络安装,随后调用创建项目,并在包命令执行结束后删除。

用软链接共享node_modules

须知node_modules使用Portable的方式管理依赖,规避了依赖树上的版本冲突,见 知乎:每个项目文件夹下都需要有node_modules吗?

1
mklink /d D:\project\B\node_modules D:\project\A\node_modules

1
npm --registry https://registry.npm.taobao.org install 

设置

1
npm config set registry https://registry.npm.taobao.org

lint

静态代码检查(static code verify),运行ng lint,根据项目目录下的tslint.json所配置的规则,检查诸如命名,空行,triple-equals等书写规范,在命令行输出违反规范的位置。
加参数—fix可自动修复绝大多数的检查错误

Unfortunately,tslint已于2019宣布停止维护,并迁移至typescript-eslint,见TSLint in 2019
关于从TSLint到typescript-eslint,参考Migrate the repo to ESLint, 实际上只需

1
npx tslint-to-eslint-config

对于Angular,关于迁移,Angular团队提出了关于性能以及与现有工具链一致性的要求,见issue#13732, 目前有angular-eslint plugin支持10.1及以上版本,以实现从tslint到eslint的迁移
1
2
3
4
5
##Step 1 - Add relevant dependencies
ng add @angular-eslint/schematics
##Step 2 - Run the convert-tslint-to-eslint schematic on a project
ng g @angular-eslint/schematics:convert-tslint-to-eslint {{YOUR_PROJECT_NAME_GOES_HERE}}
##Step 3 - Remove root TSLint configuration and use only ESLint

Karma

Karma, 业(佛教观念,个人因果的集合)Karma是测试JavaScript代码而生的自动化测试管理工具,可监控文件的变化,自动执行测试。

1
2
3
4
5
"karma": "^5.0.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.2",

Jasmine (Jasminum 茉莉)

Angular CLI 会下载并安装试用 Jasmine 测试框架 测试 Angular 应用

X.spec.ts文件用于Jasmine做单元测试
Jasmine

单元测试

单元测试(英語:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。 在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单元测试是为了测试代码逻辑,每个‘单元’在用例场景下是否能返回期望的结果,仅此而已
栗子
假设为UserService.ts设计单元测试,须知

1
2
3
4
5
6
7
export class UserService{
constructor(private commonHTTP:CommonHTTPService){ }

getUserByID(id:string):Observable<any>{
return commonHTTP.get(id)
}
}

UserService.spec.ts
1
2
3
4
5
6
7
8
9
10
describe('UserService', ()=>{
it('getUserByID return stubbed value from a spy', ()=>{
const commonHTTPSpy = jasmine.createSpyObj('CommonHTTPService', ['get']);
const stubValue = 'stub value';
commonHTTPSpy.get.and.returnValue(stubValue);

const userService = new UserService(commonHTTPSpy);
expect(userService.getUserByID(1)).toBe(stubValue,'service returned stub value')
})
});

上例待测

e2e

ng e2e builds and serves app, then runs end-to-end test with Protractor(端对端测试工具,protractor原意是量角器,Angular是角).

配置使用Headless Chrome

Angular.cn: 为在 Chrome 中运行 CI 测试而配置 CLI
karma.conf.js

1
2
3
4
5
6
7
browsers: ['ChromeHeadlessCI'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},

e2e/protractor.conf.js
1
2
3
4
5
6
7
8
9
10
const config = require('./protractor.conf').config;

config.capabilities = {
browserName: 'chrome',
chromeOptions: {
args: ['--headless', '--no-sandbox']
}
};

exports.config = config;

code coverage

1
ng test --code-coverage

或-cc输出代码覆盖率报告,其他参数见Angular Docs

Issues:Uncaught NetworkError: Failed to execute ‘send’ on ‘XMLHttpRequest’: Failed to load ‘ng:///XXXComponent/%C9%B5fac.js’.

使用—source-map=false避免test fail见StackOverflow:Angular tests failing with Failed to execute ‘send’ on ‘XMLHttpRequest’

服务端渲染(Server Side Render, SSR)

  • 通过搜索引擎优化(SEO)来帮助网络爬虫。

    根据之前写简单爬虫的经历,以模仿使用者浏览静态页面并匹配目标内容为原理的爬虫,难以识别模块化的angular页面资源,SSR方式可以让每个 URL 返回的都是一个完全渲染好的页面。

  • 迅速显示出第一个支持首次内容绘制(FCP)的页面

    据说如果页面加载超过了三秒钟,那么 53% 的移动网站会被放弃

    着陆页(landing pages),看起来就和完整的应用一样。 这些着陆页是纯 HTML,并且即使 JavaScript 被禁用了也能显示。 这些页面不会处理浏览器事件,不过它们可以用 routerLink 在这个网站中导航。

  • 提升在手机和低功耗设备上的性能

    在执行JavaScript存在困难的设备上,可以显示出静态页面,而不是直接crush。。。大概是有机会说“抱歉,blabla”

@nguniversal/express-engine 模板

1
npm add @nguniversal/express-engine --clientProject QQsNgApp

于是原angular项目变为anguar应用与express server混合项目
新增

1
2
3
4
5
./server.ts
./tsconfig.server.json
./webpack.server.config.js
./src/main.server.ts
./src/app/app.server.module.ts

篡改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
angular.json:
+ products.QQsNgApp.architect.serve={...}

main.ts:
document.addEventListener('DOMContentLoaded', () => {
bootstrap();
});

package.json:
scripts:
+ "compile:server": "webpack --config webpack.server.config.js --progress --colors",
+ "serve:ssr": "node dist/server",
+ "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
+ "build:client-and-server-bundles": "ng build --prod && ng run csd-ams-front:server:production --bundleDependencies all"
...

服务端渲染无法调用windows对象,继而document, localstorage等对象会报undefined