2019年1月2日 星期三

取物件的名稱與屬性名稱

從資料庫撈取出幾筆資料出來後對應的物件如下
public class SNMPTrapView
{
    public long Id { getset; }
 
    [Display(Name = "ID")]
    public string TrapId { getset; }
 
    [Display(Name = "時間")]
    public string TrappedOn { getset; }
 
    [Display(Name = "類型")]
    public string Type { getset; }
 
    public string Status { getset; }
 
    [Display(Name = "內容")]
    public string Content { getset; }
 
    [Display(Name = "說明")]
    public string Desc { getset; }
 
    [Display(Name = "結案")]
    public string Done { getset; }
}
案例: 想要匯出到excel,但是只想要顯示有DisplayAttribute的欄位

參考了這兩篇文章
https://stackoverflow.com/questions/7027613/how-to-retrieve-data-annotations-from-code-programmatically
https://stackoverflow.com/questions/6637679/reflection-get-attribute-name-and-value-on-property

改寫成
public static List<Tuple<stringstring>> GetPropNameAndAttrName<T>()
{
    var result = new List<Tuple<stringstring>>();
    PropertyInfo[] props = typeof(T).GetProperties();
    foreach (PropertyInfo prop in props)
    {
        object[] attrs = prop.GetCustomAttributes(true);
        if (attrs.Any())
        {
            var attr = (DisplayAttribute)attrs[0];
            if (!string.IsNullOrEmpty(attr.Name))
            {
                result.Add(new Tuple<stringstring>(prop.Name, attr.Name));
            }
        }
    }
    return result;
}

透過 .Net Core API 網站顯示
[HttpGet("obj")]
public IActionResult GetObject()
{
    return new ObjectResult(CommonTool.GetPropNameAndAttrName<SNMPTrapView>());
}

瀏覽器顯示結果




















確定如預期,完成剩下的程式,產出excel報表
// GET: api/snmptrap/excel
[HttpGet("excel")]
public async Task<IActionResult> GenerateExcelAsync(SNMPTrapSearching model)
{
    var report = await Task.Factory.StartNew(() => _snmpTrapService.Get(model));
 
    var memoryStream = new MemoryStream();
    using (var document = SpreadsheetDocument.Create(memoryStream, SpreadsheetDocumentType.Workbook))
    {
        var sheetData = CommonTool.CreateSheetData(document, "SNMPTrap");
        var props = CommonTool.GetPropNameAndAttrName<SNMPTrapView>();
        var row = new Row();
        foreach (var prop in props) // header
        {
            row.Append(new Cell()
            {
                CellValue = new CellValue(prop.Item2),
                DataType = CellValues.String
            });
        }
        sheetData.AppendChild(row);
 
        foreach (var item in report) // body
        {
            row = new Row();
            foreach (var prop in props)
            {
                var value = (string)item.GetType().GetProperty(prop.Item1).GetValue(item, null);
                row.Append(new Cell()
                {
                    CellValue = new CellValue(value),
                    DataType = CellValues.String
                });
            }
            sheetData.AppendChild(row);
        }
    }
 
    memoryStream.Seek(0SeekOrigin.Begin);
    var fileName = $"SNMPTrap_Report_{DateTime.Now.ToString("yyyyMMddHHmmssfff")}.xlsx";
    return File(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
}




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後問題就解決了。

2018年9月18日 星期二

ASP.NET Core 2.1 with MySQL raw SQL

情境:
有個案子使用 ASP.NET Core 2.1 建立 API 撈取 MySQL 資料,使用 "Pomelo.EntityFrameworkCore.MySql" 套件,操作和預設的 EntityFramework Core 一樣。

現在有一案例,客戶提供 MySQL statement 要撈取想要的資料,statement 中有 "UNION ALL",我試著轉成 Linq ,"UNION ALL" 的部分使用 "IQueryable<T>.Contact(IQueryable<T>)",在執行時發生錯誤,如果改為 "IEnumerable<T>.Contact(IEnumerable<T>)" 則可正常執行,估計這套件無法將 "IQueryable<T>.Contact(IQueryable<T>)" 轉換成正確的 MySQL statement "UNION ALL",考慮到效能所以不使用"IEnumerable<T>.Contact(IEnumerable<T>)",因此決定使用 raw SQL

            var result = new List<MyReportViewModel>();
            var ym = $"{model.Year}-{(model.Month < 10 ? "0" : "")}{model.Month}";
            using (var command = _context.Database.GetDbConnection().CreateCommand())
            {
                command.CommandText = $@"
SELECT *
FROM 
(
  {GenerateJasperMySql(ym)}

  UNION ALL

  {GenerateJasperMySql(ym, "STATEHISTORY")}

)AS SOURCE
ORDER BY object_name, start_time ASC
";
                _context.Database.OpenConnection();
                using (var reader = command.ExecuteReader())
                {
                    if (reader.HasRows)
                    {
                        while (reader.Read())
                        {
                            result.Add(new MyReportViewModel
                            {
                                object_type = (string)reader["object_type"],
                                object_id = reader.GetInt32(1),
                                object_name = (string)reader["object_name"],
                                object_service = (string)reader["object_service"],
                                start_time = (string)reader["start_time"],
                                end_time = (string)reader["end_time"],
                                object_state = (string)reader["object_state"],
                                output = (string)reader["output"],
                            });
                        }
                    }
                }
            }
            return result;

備註:
_context : 依賴注入的 DbContext
GenerateJasperMySql() : 因為 "UNION ALL" 的兩個 SQL statement 類似,因此設計這個 method 來產生
參考 使用 DataReader 擷取資料

2018年5月20日 星期日

service unavailable http error 503. the service is unavailable

開啟網站時發生異常

service unavailable http error 503. the service is unavailable

前一次開啟剛網站時還正常

Windows 10 更新以後,開啟架設在IIS上的網站(ASP.NET Core 2.0)時發生錯誤的訊息,搜尋一下得知是這個網站的 ApplicationPool 是[停用]狀態,啟用以後執行網站,還是出現同樣異常,ApplicationPool 又變成[停用]狀態,用事件檢視器查看來源為[IIS AspNetCore Module]的資訊,發現下面這則訊息

無法載入 C:\WINDOWS\System32\inetsrv\iiswsock.dll 

前幾天開發時有用到 WebSocket,也能正常使用。開啟[開啟或關閉 Windows 功能],點開[Internet Infomation Services]>[World Wide Web 服務]>[應用程式開發功能]發現之前已經勾選的[WebSocket通訊協定]是未選取狀態,勾選以後問題就解決了。

2018年5月18日 星期五

ASP.NET Core 2.0 使用 WebSocket 讓設備和網頁可以即時通訊

目前有一個專案,其中一項功能為
"接收設備端傳送過來的數據並顯示在頁面上"
例如空調系統設備傳送當前溫度

後端使用 ASP.NET Core 2.0 webapi 開發
以前有在 .Net Framework 上使用過 ASP.NET SignalR
查資料得知目前(寫這篇筆記的時候) .Net Core 版本的 SingalR 還在開發階段
2.1 版本才會發行正式版本,有些文章建議不適合在正式產品中使用開發版本
於是就查了如何在 .Net Core 使用 WebSocket

#1 [ASP.NET Core: Building chat room using WebSocket]
http://gunnarpeipman.com/aspnet/aspnet-core-websocket-chat/
上面這篇文章使用網頁發送和接收訊息

但是目前專案需求是要由設備發送訊息
#2 [Real-time chart using ASP.NET Core and WebSocket]
http://gunnarpeipman.com/aspnet/aspnet-core-websocket-chart/
上面這篇文章使用api當端點給設備端呼叫,符合專案的需求

以上兩種發送訊息的方式都想要使用,於是整合兩篇文章
使用 #1 的 SocketMiddleware 和 # 2 的 SocketManager
#2 的 SocketManager 不需修改, #1 產生 socketId 和保存當前 socket 的程式改使用 # 2 的 SocketManaer

public class ACSystemSocketMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ACSystemSocketManager _socketManager;
 
    public ChatWebSocketMiddleware(
        RequestDelegate next, 
        ACSystemSocketManager socketManager)
    {
        _next = next;
        _socketManager = socketManager;
    }
 
    public async Task Invoke(HttpContext context)
    {
        if (!context.WebSockets.IsWebSocketRequest)
        {
            await _next.Invoke(context);
            return;
        }
 
        CancellationToken ct = context.RequestAborted;
        WebSocket currentSocket = await context.WebSockets.AcceptWebSocketAsync();
 
        var socketId = _socketManager.AddSocket(currentSocket);
 
        while (true)
        {
            if (ct.IsCancellationRequested)
            {
                break;
            }
 
            var response = await ReceiveStringAsync(currentSocket, ct);
            if (string.IsNullOrEmpty(response))
            {
                if (currentSocket.State != WebSocketState.Open)
                {
                    break;
                }
 
                continue;
            }
 
            foreach (var socket in _socketManager.GetAll())
            {
                if (socket.Value.State != WebSocketState.Open)
                {
                    continue;
                }
 
                await SendStringAsync(socket.Value, response, ct);
            }
        }
 
        await _socketManager.RemoveSocket(socketId);
 
        await currentSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct);
        currentSocket.Dispose();
    }
 
    private static Task SendStringAsync(
        WebSocket socket, 
        string data, 
        CancellationToken ct = default(CancellationToken))
    {
        var buffer = Encoding.UTF8.GetBytes(data);
        var segment = new ArraySegment<byte>(buffer);
        return socket.SendAsync(segment, WebSocketMessageType.Text, true, ct);
    }
 
    private static async Task<string> ReceiveStringAsync(
        WebSocket socket, 
        CancellationToken ct = default(CancellationToken))
    {
        var buffer = new ArraySegment<byte>(new byte[8192]);
        using (var ms = new MemoryStream())
        {
            WebSocketReceiveResult result;
            do
            {
                ct.ThrowIfCancellationRequested();
 
                result = await socket.ReceiveAsync(buffer, ct);
                ms.Write(buffer.Array, buffer.Offset, result.Count);
            }
            while (!result.EndOfMessage);
 
            ms.Seek(0SeekOrigin.Begin);
            if (result.MessageType != WebSocketMessageType.Text)
            {
                return null;
            }
 
            using (var reader = new StreamReader(ms, Encoding.UTF8))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }
}

後續 :
整合過的程式執行起來正常,但是接收到的中文字串變問號,檢查後發現問題出在 #2 的 SocketManager,發送訊息(SendMessageAsync)使用的 Encoding 是ASCII,改為UTF8或是Default以後可以正常顯示中文,但是序列化成JSON格式的字串卻少了最後一個大括號,調整成下面這段程式碼以後就正常了
private async Task SendMessageAsync(WebSocket socket, string message)
{
    if (socket.State != WebSocketState.Open)
        return;
 
    var buffer = Encoding.UTF8.GetBytes(message);
    var segment = new ArraySegment<byte>(buffer);
    await socket.SendAsync(
        segment, 
        WebSocketMessageType.Text, 
        trueCancellationToken.None);
}




2018年5月14日 星期一

React & ASP.NET Core 2.0 Upload File

一個新增檔案的Form含有上傳圖片的功能並可預覽圖片,圖片與表單內容一起傳送到後端,資料新增後再存檔圖片

建立一個選取圖片的子Component
import React from 'react';
 
class ImageUpload extends React.Component {
    constructor(props) {
        super(props);
        this.state = { file: '', imagePreviewUrl: '' };
    }
 
    handleImageChange(e) {
        e.preventDefault();
 
        let reader = new FileReader();
        let file = e.target.files[0];
 
        reader.onloadend = () => {
            this.setState({
                file: file,
                imagePreviewUrl: reader.result
            });
        };
 
        reader.readAsDataURL(file);
        this.props.onImageChange(file);
    }
 
    render() {
        let { imagePreviewUrl } = this.state;
        let $imagePreview = null;
        if (imagePreviewUrl) {
            $imagePreview = <img src={imagePreviewUrl} />;
        } else {
            $imagePreview = (
                <div className="previewText">
                    Please select an Image for Preview
                </div>
            );
        }
 
        return (
            <div className="previewComponent form-group">
                <label htmlFor="file">{this.props.label}</label>
                <input
                    id="file"
                    type="file"
                    className="fileInput form-control"
                    accept="image/*"
                    onChange={e => this.handleImageChange(e)}
                />
                <div className="imgPreview">
                    {$imagePreview}
                </div>
            </div>
        );
    }
}
 
ImageUpload.defaultProps = {
    label: '上傳圖片',
    onImageChange: (file) => {
        console.log('default onImageChange() has been called', file);
    }
};
 
export default ImageUpload;

建立新增資料的Form的父Component,兩個輸入欄位的type分別為text和file
import React from 'react';
import axios from 'axios';
 
import ImageUpload from './ImageUpload';
 
class CreateProduct extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            product: '',
            file: '',
        }
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.handleImageChange = this.handleImageChange.bind(this);
    }
    handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData();
        const keys = Object.keys(this.state);
        keys.forEach(key => {
            formData.append(key, this.state[key]);
        });
        const headers = { 'Content-Type''multipart/form-data' };
        axios.post('api/product', formData, { headers: headers }).then(response => {
            console.log('create success', response);
        });
    }
    handleChange(e) {
        this.setState({
            [e.target.id]: e.target.value
        });
    }
    handleImageChange(file) {
        this.setState({ file });
    }
    render() {
        const { product } = this.state;
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <div>
                        <label htmlFor="url">Product</label>
                        <input
                            type="text"
                            id="product"
                            value={product}
                            onChange={this.handleChange}
                        />
                    </div>
                    <ImageUpload onImageChange={this.handleImageChange} />
                    <button type="submit">Submit</button>
                </form>
            </div>
        );
    }
}
 
export default CreateProduct;

後端接收資料資料的參數類型
public class ProductCreationModel
{
    public string Product{ getset; }
    public IFormFile File { getset; }
}

處理上傳的後端程式
public class AppService
{
    private readonly IHostingEnvironment _environment;
 
    public AppService(IHostingEnvironment environment)
    {
        _environment = 
            environment ?? throw new ArgumentNullException(nameof(environment));
    }
    public async Task<string> Upload(IFormFile file, string name = null)
    {
        if (string.IsNullOrWhiteSpace(_environment.WebRootPath))
        {
            _environment.WebRootPath = Path.Combine(
                Directory.GetCurrentDirectory(), "wwwroot"
                );
        }
        var dir = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        var fileName = file.FileName;
        var path = Path.Combine(dir, fileName);
        if (!string.IsNullOrEmpty(name)) // 使用自訂的檔案名稱
        {
            var extension = Path.GetExtension(path);
            fileName = $"{name}{extension}";
            path = path.Replace(file.FileName, fileName);
        }
        if (file.Length > 0)
        {
            using (var fileStream = new FileStream(path, FileMode.Create))
            {
                try
                {
                    await file.CopyToAsync(fileStream);
                    return fileName;
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
        else
        {
            throw new Exception("file is empty");
        }
    }
}




2018年3月18日 星期日

範例:ASP.NET Core 2.0 DI & EntityFrameworkCore & Repository Pattern


假設建立了一個 DbContext 叫做 TempDbContext

    public class TempDbContext : DbContext
    {
       
public TempDbContext(DbContextOptions<TempDbContext> options)  : base(options)
        {

        }

       
public DbSet<Product> Products { get; set; }
    }

IRepository<T為一些常用操作(例如CRUD)的 interface
IProductRepository 為操作 Product 的 interface,並包含了IRepository<T>

   
public interface IRepository<T> where T : class
    {
       
IEnumerable<T> GetAll();
       
IEnumerable<T> Find(Func<T, bool> predicate);
       
T GetById(int id);
       
void Create(T entity);
       
void Update(T entity);
       
void Detele(T entity);
       
int Count(Func<T, bool> predicate);
    }

   
public interface IProductRepository : IRepository<Product>
    {
        //
一些操作 Product 的方法
       
Product GetProductByName(string name);
    }

實作 IRepository<T>

    public class Repository<T> : IRepository<T> where T : class
    {
       
// 會利用 ASP.NET Core 2.0 DI 注入實例
        protected readonly TempDbContext _context;
       
public Repository(TempDbContext context)
        {
            _context = context;
        }
       
public void Save() => _context.SaveChanges();

       
public int Count(Func<T, bool> predicate)
        {
           
return _context.Set<T>().Where(predicate).Count();
        }

       
public void Create(T entity)
        {
            _context.Add(entity);
            Save();
        }

       
public void Detele(T entity)
        {
            _context.Remove(entity);
            Save();
        }

       
public IEnumerable<T> Find(Func<T, bool> predicate)
        {
           
return _context.Set<T>().Where(predicate);
        }

       
public IEnumerable<T> GetAll()
        {
           
return _context.Set<T>();
        }

       
public T GetById(int id)
        {
           
return _context.Set<T>().Find(id);
        }

       
public void Update(T entity)
        {
            _context.Entry(entity).State =
EntityState.Modified;
            Save();
        }
    }

實作 IProductRepository,也繼承 Repository<Product>

    public class ProductRepository : Repository<Product>, IProductRepository
    {
       
public ProductRepository(TempDbContext context) : base(context)
        {
        }

       
public Product GetProductByName(string name)
        {
           
return _context
                .Products
                .Where(p => p.Name == name)
                .FirstOrDefault();
        }
    }

ASP.NET Core 2.0 DI 設定(Startup)

        public void ConfigureServices(IServiceCollection services)
        {
           
// Repository 建構子需要 DbContext
            services.AddDbContext<TempDbContext>(options => options.UseInMemoryDatabase("TempContext"));
            services.AddTransient<
IProductRepository, ProductRepository>();
            services.AddMvc();
        }

Controller 中使用 Repository

    [Route("api/[controller]")]
   
public class ProductController : Controller
    {
       
private readonly IProductRepository _productRepository;

       
public ProductController(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }
    }

==========
Debug:
在執行時發生了這個錯誤

Additional information: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.
確定在ConfigureServices有設定DbContext的注入,後來發現是TempDbContext的建構子忘了呼叫base(options)