顯示具有 ASP.NET MVC 標籤的文章。 顯示所有文章
顯示具有 ASP.NET MVC 標籤的文章。 顯示所有文章

2018年10月2日 星期二

.Net應用程序將執行中的資料保存到memory會消失的問題 & 發現Application_Start被呼叫多次

案例:
IIS上有一個ASP.NET MVC網站和處理驗證功能的ASP.NET WebAPI。
網站執行的過程中會將部分資料存放到記憶體(例如驗證後取得的使用者資訊),無論是放到static property或是使用MemoryCache,過一段時間(大約30秒左右)後會發現資料消失了(session loss),查原因的過程中發現Application_Start()被重呼多次,因次得知網站被重啟所以導致保存在記憶體的資料消失了。

搜尋網站被重啟的原因,查到了這一篇文章
Reasons for ASP.NET application restarts on IIS server

在文章中看到這兩則說明和註解:
  • Aach AppPool can issue one or multiple working processes depending on the AppPool settings (Maximum Workwer Processes [maxProcesses], default is 1)

It is not a good practice to use single AppPool for hosting multiple web applications. Despite isolation in AppDomains


案例中的ASP.NET MVC網站和ASP.NET WebAPI,都使用同一個AppPool,將這兩個應用程序改使用自己專用的AppPool後問題就解決了。

2017年4月8日 星期六

將資料存放到 Seesion, 客製化 ModelBinder 來存取

假設有一個購物網站有一個CartController控制器使用Session存取Cart物件的實例
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    private Cart GetCart()
    {
        Cart cart = (Cart)Session["Cart"];
        if (cart == null)
        {
            cart = new Cart();
            Session["Cart"= cart;
        }
        return cart;
    }
}

現在要自定義一個 ModelBinder 來獲取 Session 數據中的 Cart 實例,通過實作 System.Web.Mvc.IModelBinder 介面
public class CartModelBinder : IModelBinder
{
    private const string sessionKey = "Cart";
 
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        // 通過 session 取得 Cart
        Cart cart = null;
        if (controllerContext.HttpContext.Session != null)
        {
            cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
        }
 
        // 若 session 中沒有 Cart, 則創建一個
        if (cart == null)
        {
            cart = new Cart();
            if (controllerContext.HttpContext.Session != null)
            {
                controllerContext.HttpContext.Session[sessionKey] = cart;
            }
        }
        return cart;
    }
}

需要告訴 MVC 框架,使用 CartModelBinder 來創建 Cart 實例,在Global.asax 的 Application_Start 方法中設置
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
 
        // 告訴 MVC 框架使用 CartModelBinder 類來創建 Cart 實例
        ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
    }
}

現在更新 CartController,刪除 GetCart 方法,依靠 CartModelBinder  為 CartController 提供 Cart 物件
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
}

用來創建 Cart 物件與 CartController 的邏輯方離開來了,這樣開發者能夠修改存取 Cart 物件的方法而不需要修改 CartController,可以方便對 CartController 做單元測試而不需要模仿大量的 ASP.NET 通道。
[TestMethod]
public void Can_Add_To_Cart()
{
    // Arrange
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[]
    {
        new Product{Id=1,Name="P1",Category = "Apples"},
    }.AsQueryable());
 
    Cart cart = new Cart();
 
    CartController target = new CartController(mock.Object);
 
    // Act
    target.AddToCart(cart, 1null);
 
    // Asert
    Assert.AreEqual(1, cart.Products.Count(), 1);
    Assert.AreEqual(1, cart.Products.ToArray()[0].Id);
}

2016年8月11日 星期四

drag and drop upload files with asp.net core





前端 HTML : 
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        body {
            font-family"Arial",sans-serif;
        }
 
        .dropzone {
            width300px;
            height300px;
            border2px dashed #ccc;
            color#ccc;
            line-height300px;
            text-aligncenter;
        }
 
            .dropzone.dragover {
                border-color#000;
                color#000;
            }
    </style>
</head>
<body>
    <div id="uploads"></div>
    <div class="dropzone" id="dropzone">Drop files here to append file</div>
    <ul id="files-to-upload"></ul>
    <input type="text" name="tags" id="tags" value="blue,whatever" />
    <input type="text" name="name" id="name" value="name" />
    <button onclick="onUpload()">upload</button>
    <script>
        var dropzone = document.getElementById('dropzone'),
            filesToUpload = document.getElementById('files-to-upload'),
            fileArr = [];
 
        var appendFiles = function (files) {
            console.log(files);
            for (var i = 0; i < files.length; i++) {
                filesToUpload.insertAdjacentHTML('beforeend', '<li>' + files[i].name + '</li>');
                fileArr.push(files[i]);
            }
        };
 
        var onUpload = function () {
            var xhr = new XMLHttpRequest(), formData = new FormData();
            for (var i = 0; i < fileArr.length; i++) {
                formData.append('files', fileArr[i]);
            }
            // append metadata
            formData.append('tags', document.getElementById('tags').value);
            formData.append('name', document.getElementById('name').value);
            console.log(formData);
            //
            xhr.onload = function () { // success
                var data = this.responseText;
                while (filesToUpload.firstChild) { //  faster than "filesToUpload.innerHTML = '';"
                    filesToUpload.removeChild(filesToUpload.firstChild);
                }
                fileArr = []; // empty file list
                console.log(data);
            };
            xhr.open('post', '/Home/Upload');
            xhr.send(formData);
 
        }
 
        dropzone.ondrop = function (e) {
            e.preventDefault(); // 避免瀏覽器開啟圖片
            this.className = 'dropzone';
 
            appendFiles(e.dataTransfer.files);
        };
 
        dropzone.ondragover = function () {
            this.className = 'dropzone dragover';
            return false;
        };
 
        dropzone.ondragleave = function () {
            this.className = 'dropzone';
            return false;
        };
    </script>
</body>
</html>




=========================
後端 ASP.NET Core 

[HttpPost]
public async Task<IActionResult> Upload(ICollection<IFormFile> files, string tags, string name)
{
    foreach (var file in files)
    {
        if (file.Length > 0)
        {
            using (var fileStream = new FileStream(Path.Combine("wwwroot/images", file.FileName), FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }
        }
    }
    return Content("A12345678");
}

2016年5月11日 星期三

ASP.NET 5 Self-hosting the application

在 Visual Studio 執行程式時選 web (預設是 IIS Express)

或是

在專案的資料夾([方案名稱]/src/[專案名稱])下開啟命令視窗(按Shift點右鍵會出現選項)

輸入 dnx web 後顯示  Listening on http:localhost:5000

如果要改用別的port, 修改 project.json

dependencies加入 Microsoft.AspNet.Server.WebListener
修改commandsweb(原本預設是:"Microsoft.AspNet.Server.Kestrel")


{
    "dependencies": {        
        "Microsoft.AspNet.Server.WebListener" : "1.0.0-rc-final",
    },
    "commands": {
      "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5001"  
    }
}


2015年9月2日 星期三

在 Web Api 自訂 ClaimsIdentity 和在取 token 的回傳 josn 中新增資訊

新增一個 Web Api 專案, 查看 App_Start/Startup.Auth.cs 的這個方法 :
public void ConfigureAuth(IAppBuilder app)
{
    // 設定資料庫內容和使用者管理員以針對每個要求使用單一執行個體
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
 
    // 讓應用程式使用 Cookie 儲存已登入使用者的資訊
    // 並使用 Cookie 暫時儲存使用者利用協力廠商登入提供者登入的相關資訊;
    app.UseCookieAuthentication(new CookieAuthenticationOptions());
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
    // 設定 OAuth 基礎流程的應用程式
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        // 在生產模式中設定 AllowInsecureHttp = false
        AllowInsecureHttp = true
    };
 
    // 讓應用程式使用 Bearer 權杖驗證使用者
    app.UseOAuthBearerTokens(OAuthOptions);
}
得知 OAuthAuthorizationServerOptions.Provider 使用 ApplicationOAuthProvider, 想要在取 token 所回傳的 json 中加入其他資料(例如 name 和 city), 因此針對他的 GrantResourceOwnerCredentials 方法進行修改 :
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    await Task.Run(() =>
    {
        // 自行驗證
        if (context.UserName == "ian" && context.Password == "1111")
        {
            // 驗證通過
 
            // 建立一個 ClaimsIdentity
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            // 加入一些自行命名且好讀的 Claim
            identity.AddClaim(new Claim("name""Ian"));
            identity.AddClaim(new Claim("city""Tainan"));
            // 建立一個 AuthenticationProperties
            var p1 = new Dictionary<stringstring>
            {
                {"name","Ian"}, // 顯示於回傳的json中
                {"city","Tainan"}
            };
            AuthenticationProperties properties = new AuthenticationProperties(p1);
            // 使用 ClaimsIdentity 和 AuthenticationProperties 來產生一個 AuthenticationTicket
            AuthenticationTicket ticket = new AuthenticationTicket(identity, properties);
            // 替換此內容上的票證資訊,並讓其由應用程式驗證。 呼叫之後,IsValidated 為 true 且 HasError 為 false。
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(identity);
        }
        else
        {
            context.SetError("invalid_grant""使用者名稱或密碼不正確。");
            return;
        }
    });
}

測試 :
  1. POST /token
  2. form data :
  3. username:
    ian
  4. password:
    1111
  5. grant_type:
    password
使用 jQuery.ajax 取 Token :
$.ajax({
    url: '/token',
    data: { username: username, password: password, grant_type: 'password' },
    type: 'POST',
    success: function (data) {
        console.log(data);
        $('#token').val(data.access_token);
    }
 
});

結果 :

{"access_token":"F5PU_EhpbLl7aw_EwlTuP8unNXc4L9olOlqbuan0oQbjPyh-u5iEUjQnRcs7AkV6ia7clMPn8JyZZxNucD5mP_vUTvmeGjDGZJI33qzWzehGP4xQr5HCMQ0EtaCBi7pxq0WttOtLYumoZNXmBDnRWqTtn3s7iBszewS1IHb__J2-zd1nzVmT7VKOVe_GFQhKCB_cXMqCPyfPaERLrzBYjT3ju3RYIrDn1m-ZuaLXwVM","token_type":"bearer","expires_in":1209599,"name":"Ian","city":"Tainan",".issued":"Wed, 02 Sep 2015 07:33:46 GMT",".expires":"Wed, 16 Sep 2015 07:33:46 GMT"}

name 和 city 被加入到回傳的 json 中

在後端程式中使用 ClaimsIdentity 中的資料, 例如取得使用者的 city :
[Authorize]
public class UserCityController : ApiController
{
    public string Get()
    {
        var identity = User.Identity as ClaimsIdentity;
        var city = (identity.Claims).Where(x => x.Type == "city").Select(x => x.Value).FirstOrDefault();
        return city;
    }
}

前端使用 jQuery 撈取 :
$.ajax({
    url: '/api/usercity',
    type: 'GET',
    headers: {
        'Authorization': 'Bearer ' + $('#token').val()
    },
    success: function (data) {
        $('#user-city').text(data);
    }
 
});
Authorization : Bearer {token}






2015年1月19日 星期一

AJAX 與 [Authorize]


ASP.NET MVC 在 Controller 或 Action 上加上 [Authorize] 就可以驗證是否已經登入,如果沒有登入就會被帶往登入頁面

當使用ajax方式則會得到狀態為200的頁面(指定的頁面),因此需要改寫可以回傳一個Json :

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new JsonResult
            {
                Data = new 
                { 
                    // put whatever data you want which will be sent
                    // to the client
                    message = "sorry, but you were logged out" 
                },
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

$.get('@Url.Action("SomeAction")', function (result) {
    if (result.message) {
        alert(result.message);
    } else {
        // do whatever you were doing before with the results
    }
});
如果是用 AngularJs,IsAjaxRequest 會一直判斷是 false,因為 AngularJs 的 ajax 呼叫沒有包含 X-Requested-With 表頭,而 ASP.NET MVC 是用這個表來判斷是否為一個 ajax 呼叫,所以必須改為:

var productsApp = angular.module('productsApp', []);
productsApp.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest'
}]);





2014年11月5日 星期三

自定AuthenticationFilter實現Basic認證

HTTP基本認證
當沒有訪問需要認證(還沒有認證通過)的網站時,瀏覽器自動跳出登入對話框



自定一個 Filter:
public class MyAutheticationAttribute : FilterAttributeIAuthenticationFilter
    {
        /* basic 驗證的 http header
         *     401 Unauthorizted
         *         WWW-Authenticate: Basic realm="Secure Area"
         *     請求登入
         *         Authorization: Basic {base64編碼過的登錄信息}
         */
        public const string AuthoriztionHeaderName = "Authorization";
        public const string WwwAuthenticationHeaderName = "WWW-Authenticate";
        public const string BasicAuthenticationScheme = "Basic";
        private static Dictionary<stringstring> userAccounters;
 
        static MyAutheticationAttribute()
        {
            // 為了簡單測試起見,將一些帳號密碼存放到一個靜態欄位中
            userAccounters = new Dictionary<stringstring>(StringComparer.OrdinalIgnoreCase);
            userAccounters.Add("Ian""1234");
            userAccounters.Add("Peter""1111");
            userAccounters.Add("Jay""2222");
        }
 
        // 具體的認證實現
        public void OnAuthentication(AuthenticationContext filterContext)
        {
            IPrincipal user;
            if (this.IsAuthenticated(filterContext, out user))
            {
                filterContext.Principal = user;
            }
            else
            {
                this.ProcessUnauthenticatedRequest(filterContext);
            }
        }
 
        //
        protected virtual AuthenticationHeaderValue GetAuthenticationHeaderValue(AuthenticationContext filterContext)
        {
            // Authorization 內容
            string rawValue = filterContext.RequestContext.HttpContext.Request.Headers[AuthoriztionHeaderName];
            if (string.IsNullOrEmpty(rawValue))
            {
                return null;
            }
            string[] split = rawValue.Split(' ');
            if (split.Length != 2)
            {
                return null;
            }
            return new AuthenticationHeaderValue(split[0], split[1]);
        }
 
        // 判斷是否通過驗證
        protected virtual bool IsAuthenticated(AuthenticationContext filterContext, out IPrincipal user)
        {
            user = filterContext.Principal;
            if (null != user && user.Identity.IsAuthenticated)
            {
                return true;
            }
            AuthenticationHeaderValue token = this.GetAuthenticationHeaderValue(filterContext);
            if (null != token && token.Scheme == BasicAuthenticationScheme) // "Basic"
            {
                string credential = Encoding.Default.GetString(Convert.FromBase64String(token.Parameter));
                string[] split = credential.Split(':'); // {UserNamr}:{Password}
                if (split.Length == 2)
                {
                    string userName = split[0];
                    string password;
                    if (userAccounters.TryGetValue(userName, out password))
                    {
                        if (password == split[1])
                        {
                            // 認證成功的情況下得到代表請求用戶的 Principal 對象
                            GenericIdentity identiry = new GenericIdentity(userName);
                            user = new GenericPrincipal(identiry, new string[0]);
                            return true;
                        }
                    }
                }
            }
 
            return false;
        }
 
        // 沒有通過請求
        protected virtual void ProcessUnauthenticatedRequest(AuthenticationContext filterContext)
        {
            string parameter = string.Format("realm=\"{0}\"", filterContext.RequestContext.HttpContext.Request.Url.DnsSafeHost);
            AuthenticationHeaderValue challenge = new AuthenticationHeaderValue(BasicAuthenticationScheme, parameter);
            filterContext.HttpContext.Response.Headers[WwwAuthenticationHeaderName] = challenge.ToString();
            filterContext.Result = new HttpUnauthorizedResult();
        }
 
        public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
        {
            // throw new NotImplementedException();
        }
    }

應用在Controller:
    [MyAuthetication]
public class HomeController : Controller
{
    public void Index()
    {
        Response.Write(string.Format("Controller.User: {0}<br/>"this.User.Identity.Name));
        Response.Write(string.Format("HttpContext.User: {0}<br/>"this.ControllerContext.HttpContext.User.Identity.Name));
        Response.Write(string.Format("Thread.CurrentPrincipal.Identity.Name: {0}<br/>"Thread.CurrentPrincipal.Identity.Name));
    }
}