主頁 > .NET開發 > C# Moq

C# Moq

2020-09-23 03:04:35 .NET開發

Moq


1 My Cases

        1.1 簡單入門

2 Reference

        2.1 Methods

        2.2 Matching Arguments

        2.3 Properties

        2.4 Events

        2.5 Callbacks

        2.6 Verification

        2.7 Customizing Mock Behavior

        2.8 Miscellaneous

        2.9 Advanced Features

        2.10 LINQ to Mocks

3 FAQ

        3.1 static class/method


1 My Cases

1.1 簡單入門

// 假定我有一個 MyFactory 用來創建 MyInterface 實體
// 創建 MyFactory 的 Mock 物件
var mockFactory = new Mock<MyFactory>();

// 創建 MyInterface 的 Mock 實體
var mockInstance = new Mock<MyInterface>();

// 使用 Moq 實作如果呼叫 MyInstance.DoSomething(bool) 方法無論傳入引數為何值一概拋出 MyException 例外
mockInstance.Setup(c => c.DoSomething(It.IsAny<bool>()))
    .Throws(new MyException("my exception message"));

// 使用 Moq 實作 MyFactory 的 Mock 實體第一次呼叫 CreateInstance(string) 方法時回傳 MyInterface 的 Mock 實體
// 第二次及以后呼叫則回傳真正的 MyInstance 實體
mockFactory.SetupSequence(f => f.CreateInstance(It.IsAny<string>()))
    .Returns(mockInstance.Object)
    .Returns(new MyInstance("real object"));

client.Factory = mockFactory.Object;

2 Reference

Please refer to Moq Quickstart

Moq is intended to be simple to use, strongly typed (no magic strings!, and therefore full compiler-verified and refactoring-friendly) and minimalistic (while still fully functional!).

2.1 Methods

Methods Mock

using Moq;

// Assumptions:

public interface IFoo
{
    Bar Bar { get; set; }
    string Name { get; set; }
    int Value { get; set; }
    bool DoSomething(string value);
    bool DoSomething(int number, string value);
    string DoSomethingStringy(string value);
    bool TryParse(string value, out string outputValue);
    bool Submit(ref Bar bar);
    int GetCount();
    bool Add(int value);
}

public class Bar 
{
    public virtual Baz Baz { get; set; }
    public virtual bool Submit() { return false; }
}

public class Baz
{
    public virtual string Name { get; set; }
}


var mock = new Mock<IFoo>();
mock.Setup(foo => foo.DoSomething("ping")).Returns(true);


// out arguments
var outString = "ack";
// TryParse will return true, and the out argument will return "ack", lazy evaluated
mock.Setup(foo => foo.TryParse("ping", out outString)).Returns(true);


// ref arguments
var instance = new Bar();
// Only matches if the ref argument to the invocation is the same instance
mock.Setup(foo => foo.Submit(ref instance)).Returns(true);


// access invocation arguments when returning a value
mock.Setup(x => x.DoSomethingStringy(It.IsAny<string>()))
		.Returns((string s) => s.ToLower());
// Multiple parameters overloads available


// throwing when invoked with specific parameters
mock.Setup(foo => foo.DoSomething("reset")).Throws<InvalidOperationException>();
mock.Setup(foo => foo.DoSomething("")).Throws(new ArgumentException("command"));


// lazy evaluating return value
var count = 1;
mock.Setup(foo => foo.GetCount()).Returns(() => count);


// returning different values on each invocation
var mock = new Mock<IFoo>();
var calls = 0;
mock.Setup(foo => foo.GetCount())
	.Returns(() => calls)
	.Callback(() => calls++);
// returns 0 on first invocation, 1 on the next, and so on
Console.WriteLine(mock.Object.GetCount());

2.2 Matching Arguments

Arguments Mock

// any value
mock.Setup(foo => foo.DoSomething(It.IsAny<string>())).Returns(true);


// any value passed in a `ref` parameter (requires Moq 4.8 or later):
mock.Setup(foo => foo.Submit(ref It.Ref<Bar>.IsAny)).Returns(true);


// matching Func<int>, lazy evaluated
mock.Setup(foo => foo.Add(It.Is<int>(i => i % 2 == 0))).Returns(true); 


// matching ranges
mock.Setup(foo => foo.Add(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns(true); 


// matching regex
mock.Setup(x => x.DoSomethingStringy(It.IsRegex("[a-d]+", RegexOptions.IgnoreCase))).Returns("foo");

2.3 Properties

  • Common

    Arguments Mock

    mock.Setup(foo => foo.Name).Returns("bar");
    
    
    // auto-mocking hierarchies (a.k.a. recursive mocks)
    mock.Setup(foo => foo.Bar.Baz.Name).Returns("baz");
    
    // expects an invocation to set the value to "foo"
    mock.SetupSet(foo => foo.Name = "foo");
    
    // or verify the setter directly
    mock.VerifySet(foo => foo.Name = "foo");
    
  • Setup a property so that it will automatically start tracking its value (also known as Stub)

    Arguments Mock

    // start "tracking" sets/gets to this property
    mock.SetupProperty(f => f.Name);
    
    // alternatively, provide a default value for the stubbed property
    mock.SetupProperty(f => f.Name, "foo");
    
    
    // Now you can do:
    
    IFoo foo = mock.Object;
    // Initial value was stored
    Assert.Equal("foo", foo.Name);
    
    // New value set which changes the initial value
    foo.Name = "bar";
    Assert.Equal("bar", foo.Name);
    
  • Stub all properties on a mock (not available on Silverlight)

    mock.SetupAllProperties();
    

2.4 Events

Events Mock

// Raising an event on the mock
mock.Raise(m => m.FooEvent += null, new FooEventArgs(fooValue));

// Raising an event on the mock that has sender in handler parameters
mock.Raise(m => m.FooEvent += null, this, new FooEventArgs(fooValue));

// Raising an event on a descendant down the hierarchy
mock.Raise(m => m.Child.First.FooEvent += null, new FooEventArgs(fooValue));

// Causing an event to raise automatically when Submit is invoked
mock.Setup(foo => foo.Submit()).Raises(f => f.Sent += null, EventArgs.Empty);
// The raised event would trigger behavior on the object under test, which 
// you would make assertions about later (how its state changed as a consequence, typically)

// Raising a custom event which does not adhere to the EventHandler pattern
public delegate void MyEventHandler(int i, bool b);
public interface IFoo
{
  event MyEventHandler MyEvent; 
}

var mock = new Mock<IFoo>();
...
// Raise passing the custom arguments expected by the event delegate
mock.Raise(foo => foo.MyEvent += null, 25, true);

2.5 Callbacks

Mock Callback

var mock = new Mock<IFoo>();
var calls = 0;
var callArgs = new List<string>();

mock.Setup(foo => foo.DoSomething("ping"))
    .Returns(true)
    .Callback(() => calls++);

// access invocation arguments
mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
    .Returns(true)
    .Callback((string s) => callArgs.Add(s));

// alternate equivalent generic method syntax
mock.Setup(foo => foo.DoSomething(It.IsAny<string>()))
    .Returns(true)
    .Callback<string>(s => callArgs.Add(s));

// access arguments for methods with multiple parameters
mock.Setup(foo => foo.DoSomething(It.IsAny<int>(), It.IsAny<string>()))
    .Returns(true)
    .Callback<int, string>((i, s) => callArgs.Add(s));

// callbacks can be specified before and after invocation
mock.Setup(foo => foo.DoSomething("ping"))
    .Callback(() => Console.WriteLine("Before returns"))
    .Returns(true)
    .Callback(() => Console.WriteLine("After returns"));

// callbacks for methods with `ref` / `out` parameters are possible but require some work (and Moq 4.8 or later):
delegate void SubmitCallback(ref Bar bar);

mock.Setup(foo => foo.Submit(ref It.Ref<Bar>.IsAny)
    .Callback(new SubmitCallback((ref Bar bar) => Console.WriteLine("Submitting a Bar!"));

2.6 Verification

Mock Verification

mock.Verify(foo => foo.DoSomething("ping"));

// Verify with custom error message for failure
mock.Verify(foo => foo.DoSomething("ping"), "When doing operation X, the service should be pinged always");

// Method should never be called
mock.Verify(foo => foo.DoSomething("ping"), Times.Never());

// Called at least once
mock.Verify(foo => foo.DoSomething("ping"), Times.AtLeastOnce());

// Verify getter invocation, regardless of value.
mock.VerifyGet(foo => foo.Name);

// Verify setter invocation, regardless of value.
mock.VerifySet(foo => foo.Name);

// Verify setter called with specific value
mock.VerifySet(foo => foo.Name ="foo");

// Verify setter with an argument matcher
mock.VerifySet(foo => foo.Value = It.IsInRange(1, 5, Range.Inclusive));

// Verify that no other invocations were made other than those already verified (requires Moq 4.8 or later)
mock.VerifyNoOtherCalls();

2.7 Customizing Mock Behavior

  • Make mock behave like a "true Mock", raising exceptions for anything that doesn't have a corresponding expectation: in Moq slang a "Strict" mock; default behavior is "Loose" mock, which never throws and returns default values or empty arrays, enumerables, etc. if no expectation is set for a member

    var mock = new Mock<IFoo>(MockBehavior.Strict);
    
  • Invoke base class implementation if no expectation overrides the member (a.k.a. "Partial Mocks" in Rhino Mocks): default is false. (this is required if you are mocking Web/Html controls in System.Web!)

    var mock = new Mock<IFoo> { CallBase = true };
    
  • Make an automatic recursive mock: a mock that will return a new mock for every member that doesn't have an expectation and whose return value can be mocked (i.e. it is not a value type)

    var mock = new Mock<IFoo> { DefaultValue = DefaultValue.Mock };
    // default is DefaultValue.Empty
    
    // this property access would return a new mock of Bar as it's "mock-able"
    Bar value = mock.Object.Bar;
    
    // the returned mock is reused, so further accesses to the property return 
    // the same mock instance. this allows us to also use this instance to 
    // set further expectations on it if we want
    var barMock = Mock.Get(value);
    barMock.Setup(b => b.Submit()).Returns(true);
    
  • Centralizing mock instance creation and management: you can create and verify all mocks in a single place by using a MockRepository, which allows setting the MockBehavior, its CallBase and DefaultValue consistently

    var repository = new MockRepository(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };
    
    // Create a mock using the repository settings
    var fooMock = repository.Create<IFoo>();
    
    // Create a mock overriding the repository settings
    var barMock = repository.Create<Bar>(MockBehavior.Loose);
    
    // Verify all verifiable expectations on all mocks created through the repository
    repository.Verify();
    

2.8 Miscellaneous

  • Setting up a member to return different values / throw exceptions on sequential calls:

    var mock = new Mock<IFoo>();
    mock.SetupSequence(f => f.GetCount())
        .Returns(3)  // will be returned on 1st invocation
        .Returns(2)  // will be returned on 2nd invocation
        .Returns(1)  // will be returned on 3rd invocation
        .Returns(0)  // will be returned on 4th invocation
        .Throws(new InvalidOperationException());  // will be thrown on 5th invocation
    
  • Setting expectations for protected members (you can't get IntelliSense for these, so you access them using the member name as a string):

    // at the top of the test fixture
    using Moq.Protected;
    
    // in the test
    var mock = new Mock<CommandBase>();
    mock.Protected()
         .Setup<int>("Execute")
         .Returns(5);
    
    // if you need argument matching, you MUST use ItExpr rather than It
    // planning on improving this for vNext (see below for an alternative in Moq 4.8)
    mock.Protected()
        .Setup<bool>("Execute",
            ItExpr.IsAny<string>())
        .Returns(true);
    
  • Moq 4.8 and later allows you to set up protected members through a completely unrelated type that has the same members and thus provides the type information necessary for IntelliSense to work. You can also use this interface to set up protected generic methods and those having by-ref parameters:

    interface CommandBaseProtectedMembers
    {
        bool Execute(string arg);
    }
    
    mock.Protected().As<CommandBaseProtectedMembers>()
        .Setup(m => m.Execute(It.IsAny<string>()))  // will set up CommandBase.Execute
        .Returns(true);
    

2.9 Advanced Features

  • Common

    // get mock from a mocked instance
    IFoo foo = // get mock instance somehow
    var fooMock = Mock.Get(foo);
    fooMock.Setup(f => f.GetCount()).Returns(42);
    
    
    // implementing multiple interfaces in mock
    var mock = new Mock<IFoo>();
    var disposableFoo = mock.As<IDisposable>();
    // now the IFoo mock also implements IDisposable :)
    disposableFoo.Setup(disposable => disposable.Dispose());
    
    // implementing multiple interfaces in single mock
    var mock = new Mock<IFoo>();
    mock.Setup(foo => foo.Name).Returns("Fred");
    mock.As<IDisposable>().Setup(disposable => disposable.Dispose());
    
    
    // custom matchers
    mock.Setup(foo => foo.DoSomething(IsLarge())).Throws<ArgumentException>();
    ...
    public string IsLarge() 
    { 
      return Match.Create<string>(s => !String.IsNullOrEmpty(s) && s.Length > 100);
    }
    
  • Mocking internal types: Add either of the following custom attributes (typically in AssemblyInfo.cs) to the project containing the internal types — which one you need depends on whether your own project is strong-named or not:

    // This assembly is the default dynamic assembly generated by Castle DynamicProxy, 
    // used by Moq. If your assembly is strong-named, paste the following in a single line:
    [assembly:InternalsVisibleTo("DynamicProxyGenAssembly2,PublicKey=00...cc7")]
    
    // Or, if your own assembly is not strong-named, omit the public key:
    [assembly:InternalsVisibleTo("DynamicProxyGenAssembly2")]
    
  • Starting in Moq 4.8, you can create your own custom default value generation strategies (besides DefaultValue.Empty and DefaultValue.Mock) by subclassing DefaultValueProvider, or, if you want some more convenience, LookupOrFallbackDefaultValueProvider:

    class MyEmptyDefaultValueProvider : LookupOrFallbackDefaultValueProvider
    {
        public MyEmptyDefaultValueProvider()
        {
            base.Register(typeof(string), (type, mock) => "?");
            base.Register(typeof(List<>), (type, mock) => Activator.CreateInstance(type));
        }
    }
    
    var mock = new Mock<IFoo> { DefaultValueProvider = new MyEmptyDefaultValueProvider() };
    var name = mock.Object.Name;  // => "?"
    

    Note: When you pass the mock for consumption, you must pass mock.Object, not mock itself.

2.10 LINQ to Mocks

Moq is the one and only mocking framework that allows specifying mock behavior via declarative specification queries. You can think of LINQ to Mocks as:

Keep that query form in mind when reading the specifications:

var services = Mock.Of<IServiceProvider>(sp =>
    sp.GetService(typeof(IRepository)) == Mock.Of<IRepository>(r => r.IsAuthenticated == true) &&
    sp.GetService(typeof(IAuthentication)) == Mock.Of<IAuthentication>(a => a.AuthenticationType == "OAuth"));

// Multiple setups on a single mock and its recursive mocks
ControllerContext context = Mock.Of<ControllerContext>(ctx =>
     ctx.HttpContext.User.Identity.Name == "kzu" &&
     ctx.HttpContext.Request.IsAuthenticated == true &&
     ctx.HttpContext.Request.Url == new Uri("http://moqthis.com") &&
     ctx.HttpContext.Response.ContentType == "application/xml");

// Setting up multiple chained mocks:
var context = Mock.Of<ControllerContext>(ctx =>
     ctx.HttpContext.Request.Url == new Uri("http://moqthis.me") &&
     ctx.HttpContext.Response.ContentType == "application/xml" &&
     // Chained mock specification
     ctx.HttpContext.GetSection("server") == Mock.Of<ServerSection>(config =>
         config.Server.ServerUrl == new Uri("http://moqthis.com/api"));

LINQ to Mocks is great for quickly stubbing out dependencies that typically don't need further verification. If you do need to verify later some invocation on those mocks, you can easily retrieve them with Mock.Get(instance).

3 FAQ

3.1 static class/method

Moq 不支持對靜態類或方法的 mock. 作為變通,可以使用實體方法來呼叫靜態方法,再去 mock 實體方法,

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/108743.html

標籤:C#

上一篇:執行緒基礎

下一篇:System.IO.Pipelines——高性能IO(一)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more