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

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年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月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)

2017年5月18日 星期四

在 ASP.NET Core 自定義中間件(middleware)使用依賴注入(DI)

很多ASP.NET Core專案可能需要自定義 middleware,所以筆記一下用簡單的方法自製一個middleware 並用內建的 dependency injection 系統

使用  ASP.NET Core 1.1


ASP.NET Core middleware  方法被定義在Startup類別的Configure方法裡,當應用程式啟動的時候,框架會自動執行這個方法,並提供IApplicationBuilder實例,使用該實例的Use方法註冊自定義 middleware
IApplicationBuilder Use(Func<RequestDelegateRequestDelegate> middleware);
Use方法接受一個Func參數,該參數接受和返回都是 RequestDelegate 
public delegate Task RequestDelegate(HttpContext context)
[範例 1]:
app.Use(next =>
{
    return async context =>
    {
        await context.Response.WriteAsync("Hello World!");
        await next(context);
    };
});
用 lambda 表示 Func<RequestDelegateRequestDelegate>,參數 next 是下一個middle;return的是一個新的 RequestDelegate,他接收 HttpContext 實例  

除了[範例 1]以外,同個命名空間(Microsoft.AspNetCore.Builder)下有一個擴展方法寫法較為簡短
[範例 2]:
app.Use(async (ctx, next) =>
{
    await ctx.Response.WriteAsync("Hello World!");
    await next();
});
他接收的參數類型:
Func<HttpContextFunc<Task>, Task>
如果是最後一個middleware, 可以使用 Run(就像Use沒用next)
app.Run(async context => await context.Response.WriteAsync("Hi"));

另一個建立middleware的方式允許在class裡封裝邏輯,該class必須有接受HttpContext的Invoke方法:
public class MyMiddleware
{
    private readonly RequestDelegate _next;
 
    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task Invoke(HttpContext context)
    {
        await context.Response.WriteAsync("Hi!");
        await _next(context);
    }
}
該方法必須有一個接受RequestDelegate實例的建構式,可以在Invoke方法裡去呼叫下一個middleware。

使用自定義middleware的方式:
app.UseMiddleware<MyMiddleware>();
或是擴展
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<MyMiddleware>();
    return app;
}
然後使用他
app.UseMyMiddleware();

如果middleware需要呼叫另一個物件的方法,可以在middleware裡面建立物件,更優雅的寫法是使用內建的dependency injection

Startup類別的ConfigureServices方法可以註冊依賴物件,該方法比Configure早執行,所以所有的依賴物件可以被middleware使用

建立interface和class:
public interface IGreeter
{
    string Greet();
}
public class Greeter : IGreeter
{
    public string Greet()
    {
        return "Hi by Greeter!";
    }
}
在ConfigureServices方法裡面註冊:
services.AddTransient<IGreeterGreeter>();
AddTransient方法在每次request時都會建立一個實例,其他還有AddSingleton/AddScoped或是簡單的Add,整個DI系統的描述在docs.asp.net/dependency-injection 

完成註冊後,直接在 Configure 方法使用:
app.Use(async (context, next) =>
{
    IGreeter greeter = context.RequestServices.GetService<IGreeter>();
    await context.Response.WriteAsync(greeter.Greet());
    await next();
});
或是寫在自定義middleware:
public class MyMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IGreeter _greeter;
 
        public MyMiddleware(RequestDelegate next, IGreeter greeter)
        {
            _next = next;
            _greeter = greeter;
        }
 
        public async Task Invoke(HttpContext context)
        {
            await context.Response.WriteAsync(_greeter.Greet());
            await _next(context);
        }
    }
如果DI系統知道這些參數的類型,則在 class 被實例化時,它們將被自動解析