Declare Dependency Injection when the constructor takes a parameter.

huangapple go评论98阅读模式
英文:

Declare Dependency Injection when the constructor takes a paramter

问题

在.NET 6.0 Core控制台应用程序中学习DI
我已经编写了这个:

  1. static void Main(string[] args)
  2. {
  3. var serviceProvider = new ServiceCollection()
  4. .AddSingleton<InterfaceOne, ClassOne>()
  5. .AddSingleton<InterfaceTwo, ClassTwo>()
  6. .BuildServiceProvider();
  7. var one = serviceProvider.GetRequiredService<InterfaceOne>(); // 可行。它的构造函数没有参数。
  8. var two = serviceProvider.GetRequiredService<InterfaceTwo>(); // 不可行。它的构造函数有一个参数。
  9. one.JustSayHi();
  10. //two.SayWhateverMessageIsPassedToYou();
  11. }

ClassOne 实现了 InterfaceOne,它非常简单,在 constructor 中没有特殊的内容,所以我能够运行它。但是后来我创建了 ClassTwoInterfaceTwo,但现在我不知道如何将参数传递给该类。这是我的 ClassTwo

  1. public interface InterfaceTwo
  2. {
  3. void SayWhateverMessageIsPassedToYou();
  4. }
  5. public class ClassTwo : InterfaceTwo
  6. {
  7. private readonly string _sayThisThing;
  8. public ClassTwo(string sayThisThing)
  9. {
  10. _sayThisThing = sayThisThing;
  11. }
  12. public void SayWhateverMessageIsPassedToYou()
  13. {
  14. Console.WriteLine(_sayThisThing);
  15. }
  16. }
英文:

Learning DI in a .NET 6.0 Core Console Application:
I have written this:

  1. static void Main(string[] args)
  2. {
  3. var serviceProvider = new ServiceCollection()
  4. .AddSingleton&lt;InterfaceOne, ClassOne&gt;()
  5. .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;()
  6. .BuildServiceProvider();
  7. var one = serviceProvider.GetRequiredService&lt;InterfaceOne&gt;(); // works. Its CTOR has no params.
  8. var two = serviceProvider.GetRequiredService&lt;InterfaceTwo&gt;(); // does not work. Its CTOR has a param.
  9. one.JustSayHi();
  10. //two.SayWhateverMessageIsPassedToYou();
  11. }

ClassOne implements InterfaceOne and it is super simple, nothing special in constructor so I was able to run that one. But then I created ClassTwo and InterfaceTwo but I don't know now how to pass my parameter to that class. Here is my ClassTwo :

  1. public interface InterfaceTwo
  2. {
  3. void SayWhateverMessageIsPassedToYou();
  4. }
  5. public class ClassTwo: InterfaceTwo
  6. {
  7. private readonly string _sayThisThing;
  8. public ClassTwo(string sayThisThing)
  9. {
  10. _sayThisThing = sayThisThing;
  11. }
  12. public void SayWhateverMessageIsPassedToYou()
  13. {
  14. Console.WriteLine(_sayThisThing);
  15. }
  16. }

答案1

得分: 2

Brian的回答在详细说明了一些选项的同时,跳过了我认为最好的选项,如果可能的话:将您的代码重构以将服务与值分开。

例如,如果字符串值可能会随着应用程序的上下文而变化(例如,它作为请求参数提供),那么接口应该被更改为将字符串值作为方法参数:

  1. public interface InterfaceTwo
  2. {
  3. void SayWhateverMessageIsPassedToYou(string sayThisThing);
  4. }

...然后,您的实现可以使用其构造函数来注入它所使用的服务。例如,注入控制台将使该类更容易在隔离的单元测试中进行测试:

  1. public class ClassTwo : InterfaceTwo
  2. {
  3. TextWriter _output;
  4. public ClassTwo(TextWriter output)
  5. {
  6. _output = output;
  7. }
  8. public void SayWhateverMessageIsPassedToYou(string sayThisThing)
  9. {
  10. _output.WriteLine(sayThisThing);
  11. }
  12. }
  1. var serviceProvider = new ServiceCollection()
  2. .AddSingleton<InterfaceTwo, ClassTwo>()
  3. .AddSingleton<TextWriter>(Console.Out)
  4. .BuildServiceProvider();

如果字符串不随上下文而变化,因此更改方法签名没有意义,考虑注入一个独立的服务,该服务充当该字符串的提供者,供您的类使用。

  1. public class ClassTwo : InterfaceTwo
  2. {
  3. TextWriter _output;
  4. IMessageProvider _messageProvider;
  5. public ClassTwo(TextWriter output, IMessageProvider messageProvider)
  6. {
  7. _output = output;
  8. _messageProvider = messageProvider;
  9. }
  10. public void SayWhateverMessageIsPassedToYou()
  11. {
  12. _output.WriteLine(_messageProvider.GetMessage());
  13. }
  14. }
  15. public interface IMessageProvider
  16. {
  17. string GetMessage();
  18. }
  19. public record FixedMessageProvider(string Message) : IMessageProvider
  20. {
  21. public string GetMessage() => Message;
  22. }
  1. var serviceProvider = new ServiceCollection()
  2. .AddSingleton<InterfaceTwo, ClassTwo>()
  3. .AddSingleton<TextWriter>(Console.Out)
  4. .AddSingleton<IMessageProvider>(new FixedMessageProvider("Hi"))
  5. .BuildServiceProvider();

这是用于在ASP.NET Core中获取配置选项的模式示例:https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0

英文:

While Brian's answer does a great job at spelling out some options, it skips the option which I would consider best of all, if it's possible: Refactor your code to separate Services from Values.

For example, if the string value might change with the context of your application (e.g. it's provided as a request parameter), then the interface should be changed to take the string value as a method parameter:

  1. public interface InterfaceTwo
  2. {
  3. void SayWhateverMessageIsPassedToYou(string sayThisThing);
  4. }

... then your implementation could use its constructor to inject the services that it uses. For example, injecting the console would make the class easier to unit test in isolation:

  1. public class ClassTwo : InterfaceTwo
  2. {
  3. TextWriter _output;
  4. public ClassTwo(TextWriter output)
  5. {
  6. _output = output;
  7. }
  8. public void SayWhateverMessageIsPassedToYou(string sayThisThing)
  9. {
  10. _output.WriteLine(sayThisThing);
  11. }
  12. }
  1. var serviceProvider = new ServiceCollection()
  2. .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;()
  3. .AddSingleton&lt;TextWriter&gt;(Console.Out)
  4. .BuildServiceProvider();

If the string doesn't change with context, so it doesn't make sense to change the method signature, consider injecting a separate service which acts as the provider of that string for your class to use.

  1. public class ClassTwo : InterfaceTwo
  2. {
  3. TextWriter _output;
  4. IMessageProvider _messageProvider;
  5. public ClassTwo(TextWriter output, IMessageProvider messageProvider)
  6. {
  7. _output = output;
  8. _messageProvider = messageProvider;
  9. }
  10. public void SayWhateverMessageIsPassedToYou()
  11. {
  12. _output.WriteLine(_messageProvider.GetMessage());
  13. }
  14. }
  15. public interface IMessageProvider
  16. {
  17. string GetMessage();
  18. }
  19. public record FixedMessageProvider(string Message) : IMessageProvider
  20. {
  21. public string GetMessage() =&gt; Message;
  22. }
  1. var serviceProvider = new ServiceCollection()
  2. .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;()
  3. .AddSingleton&lt;TextWriter&gt;(Console.Out)
  4. .AddSingleton&lt;IMessageProvider&gt;(new FixedMessageProvider(&quot;Hi&quot;))
  5. .BuildServiceProvider();

This is the pattern used, for example, to get configuration options in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0

答案2

得分: 1

<h1>方法 1</h1>
为了直接回答您的问题,虽然我不建议这种方法:

  1. var serviceProvider = new ServiceCollection()
  2. .AddSingleton&lt;InterfaceOne, ClassOne&gt;()
  3. .AddSingleton&lt;InterfaceTwo, ClassTwo&gt;(sp =&gt; new ClassTwo(&quot;Hello, World!&quot;))
  4. .BuildServiceProvider();

以下是完整上下文中的答案,附带了一个示例控制台应用程序...
https://github.com/CodeFontana/FoobarConsoleApp

FoobarConsoleUI.csproj

  1. &lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
  2. &lt;PropertyGroup&gt;
  3. &lt;OutputType&gt;Exe&lt;/OutputType&gt;
  4. &lt;TargetFramework&gt;net7.0&lt;/TargetFramework&gt;
  5. &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
  6. &lt;Nullable&gt;enable&lt;/Nullable&gt;
  7. &lt;/PropertyGroup&gt;
  8. &lt;ItemGroup&gt;
  9. &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;7.0.1&quot; /&gt;
  10. &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting.Abstractions&quot; Version=&quot;7.0.0&quot; /&gt;
  11. &lt;/ItemGroup&gt;
  12. &lt;/Project&gt;

Program.cs

  1. using FoobarConsoleUI;
  2. using FoobarConsoleUI.Interfaces;
  3. using FoobarConsoleUI.Services;
  4. using Microsoft.Extensions.DependencyInjection;
  5. using Microsoft.Extensions.Hosting;
  6. using Microsoft.Extensions.Logging;
  7. HostApplicationBuilder builder = Host.CreateApplicationBuilder();
  8. builder.Services.AddTransient&lt;IFooService, FooService&gt;();
  9. builder.Services.AddTransient&lt;IBarService, BarService&gt;(sp =&gt;
  10. {
  11. ILogger&lt;BarService&gt; barLogger = sp.GetRequiredService&lt;ILogger&lt;BarService&gt;&gt;();
  12. return new BarService(barLogger, &quot;Foobar&quot;);
  13. });
  14. builder.Services.AddHostedService&lt;App&gt;();
  15. IHost app = builder.Build();
  16. app.Run();

App.cs

  1. using FoobarConsoleUI.Interfaces;
  2. using Microsoft.Extensions.Configuration;
  3. using Microsoft.Extensions.Hosting;
  4. using Microsoft.Extensions.Logging;
  5. namespace FoobarConsoleUI;
  6. public class App : IHostedService
  7. {
  8. private readonly IHostApplicationLifetime _hostApplicationLifetime;
  9. private readonly IConfiguration _config;
  10. private readonly ILogger&lt;App&gt; _logger;
  11. private readonly IFooService _fooService;
  12. private readonly IBarService _barService;
  13. public App(IHostApplicationLifetime hostApplicationLifetime,
  14. IConfiguration configuration,
  15. ILogger&lt;App&gt; logger,
  16. IFooService fooService,
  17. IBarService barService)
  18. {
  19. _hostApplicationLifetime = hostApplicationLifetime;
  20. _config = configuration;
  21. _logger = logger;
  22. _fooService = fooService;
  23. _barService = barService;
  24. }
  25. public Task StartAsync(CancellationToken cancellationToken)
  26. {
  27. _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
  28. {
  29. try
  30. {
  31. await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
  32. await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
  33. Execute();
  34. }
  35. catch (Exception ex)
  36. {
  37. _logger.LogError(ex, &quot;Unhandled exception!&quot;);
  38. }
  39. finally
  40. {
  41. _hostApplicationLifetime.StopApplication();
  42. }
  43. });
  44. return Task.CompletedTask;
  45. }
  46. public Task StopAsync(CancellationToken cancellationToken)
  47. {
  48. return Task.CompletedTask;
  49. }
  50. public void Execute()
  51. {
  52. _fooService.Foo();
  53. _barService.Bar();
  54. }
  55. }

IFooService:

  1. namespace FoobarConsoleUI.Interfaces;
  2. public interface IFooService
  3. {
  4. void Foo();
  5. }

FooService

  1. using FoobarConsoleUI.Interfaces;
  2. using Microsoft.Extensions.Logging;
  3. namespace FoobarConsoleUI.Services;
  4. public class FooService : IFooService
  5. {
  6. private readonly ILogger&lt;FooService&gt; _logger;
  7. public FooService(ILogger&lt;FooService&gt; logger)
  8. {
  9. _logger = logger;
  10. }
  11. public void Foo()
  12. {
  13. _logger.LogInformation(&quot;Foo&quot;);
  14. }
  15. }

IBarService:

  1. namespace FoobarConsoleUI.Interfaces;
  2. public interface IBarService
  3. {
  4. void Bar();
  5. }

BarService:

  1. using FoobarConsoleUI.Interfaces;
  2. using Microsoft.Extensions.Logging;
  3. namespace FoobarConsoleUI.Services;
  4. public class BarService : IBarService
  5. {
  6. private readonly ILogger&lt;BarService&gt; _logger;
  7. private readonly string _inputText;
  8. public BarService(ILogger&lt;BarService&gt; logger,
  9. string inputText)
  10. {
  11. _logger = logger;
  12. _inputText = inputText;
  13. }
  14. public void Bar()
  15. {
  16. _logger.LogInformation($&quot;Bar: {_inputText}&quot;);
  17. }
  18. }

输出:
Declare Dependency Injection when the constructor takes a parameter.

<h1>方法 2</h1>
如评论中所讨论的,简单地不要求构造函数参数,而是使用属性。在这种方法中,在执行方法之前在运行时设置属性。这种方法的缺点是逻辑错误,即在设置属性之前调用SayWhateverMessageIsPassedToYou()方法。所以要小心。

我认为这种方法很简单,所以我不会提供代码示例。

<h1>方法 3</h1>
在某些情况下,您将不得不在运行时传递构造函数参数,这是您最终会遇到的情况。因此,最好的方法是实现一个工厂类,该类将被注入到您的DI中,并且在运行时可以使用正确的参数实例化您所需的类。

以下是从方法1中重新编写的相同程序,以使用工厂类...
Program.cs

  1. using FoobarFactoryConsoleUI;
  2. using FoobarFactoryConsoleUI.Factories;
  3. using FoobarFactoryConsoleUI.Interfaces;
  4. using FoobarFactoryConsoleUI.Services;
  5. using Microsoft.Extensions.DependencyInjection;
  6. using Microsoft.Extensions.Hosting;
  7. using Microsoft.Extensions.Logging;
  8. HostApplicationBuilder builder = Host.CreateApplicationBuilder();
  9. builder.Services.AddTransient&lt;IFooService, FooService&gt;();
  10. builder.Services.AddTransient&lt;BarServiceFactory&gt;();
  11. builder.Services.AddHostedService&lt;App&gt;();
  12. IHost app = builder.Build();
  13. app.Run();

App.cs

  1. using FoobarFactoryConsoleUI.Factories;
  2. using FoobarFactoryConsoleUI.Interfaces;
  3. using Microsoft.Extensions.Configuration;
  4. using Microsoft.Extensions.Hosting;
  5. using Microsoft.Extensions.Logging;
  6. namespace FoobarFactoryConsoleUI;
  7. public class App
  8. <details>
  9. <summary>英文:</summary>
  10. &lt;h1&gt;Method 1&lt;/h1&gt;
  11. To most directly answer your question, though I would discourage this approach:

var serviceProvider = new ServiceCollection()
.AddSingleton<InterfaceOne, ClassOne>()
.AddSingleton<InterfaceTwo, ClassTwo>(sp => new ClassTwo("Hello, World!"))
.BuildServiceProvider();

  1. Here&#39;s the answer in full context with a sample console app...
  2. https://github.com/CodeFontana/FoobarConsoleApp
  3. FoobarConsoleUI.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
</ItemGroup>

</Project>

  1. Program.cs

using FoobarConsoleUI;
using FoobarConsoleUI.Interfaces;
using FoobarConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<IBarService, BarService>(sp =>
{
ILogger<BarService> barLogger = sp.GetRequiredService<ILogger<BarService>>();
return new BarService(barLogger, "Foobar");
});
builder.Services.AddHostedService<App>();

IHost app = builder.Build();
app.Run();

  1. App.cs

using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace FoobarConsoleUI;

public class App : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _config;
private readonly ILogger<App> _logger;
private readonly IFooService _fooService;
private readonly IBarService _barService;

  1. public App(IHostApplicationLifetime hostApplicationLifetime,
  2. IConfiguration configuration,
  3. ILogger&lt;App&gt; logger,
  4. IFooService fooService,
  5. IBarService barService)
  6. {
  7. _hostApplicationLifetime = hostApplicationLifetime;
  8. _config = configuration;
  9. _logger = logger;
  10. _fooService = fooService;
  11. _barService = barService;
  12. }
  13. public Task StartAsync(CancellationToken cancellationToken)
  14. {
  15. _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
  16. {
  17. try
  18. {
  19. await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
  20. await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
  21. Execute();
  22. }
  23. catch (Exception ex)
  24. {
  25. _logger.LogError(ex, &quot;Unhandled exception!&quot;);
  26. }
  27. finally
  28. {
  29. _hostApplicationLifetime.StopApplication();
  30. }
  31. });
  32. return Task.CompletedTask;
  33. }
  34. public Task StopAsync(CancellationToken cancellationToken)
  35. {
  36. return Task.CompletedTask;
  37. }
  38. public void Execute()
  39. {
  40. _fooService.Foo();
  41. _barService.Bar();
  42. }

}

  1. IFooService:

namespace FoobarConsoleUI.Interfaces;

public interface IFooService
{
void Foo();
}

  1. FooService

using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;

namespace FoobarConsoleUI.Services;

public class FooService : IFooService
{
private readonly ILogger<FooService> _logger;

  1. public FooService(ILogger&lt;FooService&gt; logger)
  2. {
  3. _logger = logger;
  4. }
  5. public void Foo()
  6. {
  7. _logger.LogInformation(&quot;Foo&quot;);
  8. }

}

  1. IBarService:

namespace FoobarConsoleUI.Interfaces;

public interface IBarService
{
void Bar();
}

  1. BarService:

using FoobarConsoleUI.Interfaces;
using Microsoft.Extensions.Logging;

namespace FoobarConsoleUI.Services;

public class BarService : IBarService
{
private readonly ILogger<BarService> _logger;
private readonly string _inputText;

  1. public BarService(ILogger&lt;BarService&gt; logger,
  2. string inputText)
  3. {
  4. _logger = logger;
  5. _inputText = inputText;
  6. }
  7. public void Bar()
  8. {
  9. _logger.LogInformation($&quot;Bar: {_inputText}&quot;);
  10. }

}

  1. Output:
  2. [![enter image description here][1]][1]
  3. [1]: https://i.stack.imgur.com/bmbmG.png
  4. &lt;h1&gt;Method 2&lt;/h1&gt;
  5. As discussed in the comments, simply don&#39;t require the constructor parameter, and use a property instead. In this approach, the property is passed at runtime before executing your method.
  6. The downside to this approach is a logic error, where you call your SayWhateverMessageIsPassedToYou() before your property is set. So be careful.
  7. I think this approach is straightforward, so I won&#39;t provide a code sample.
  8. &lt;h1&gt;Method 3&lt;/h1&gt;
  9. In the scenario, which you will run into eventually, sometimes you just have to pass a constructor parameter at runtime. Thus the best approach is to implement a factory class, which would get injected into your DI, and at runtime can instantiate your required class with the proper parameters.
  10. Here&#39;s the same program from Method 1, rewritten to use a factory class...
  11. Program.cs

using FoobarFactoryConsoleUI;
using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using FoobarFactoryConsoleUI.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder();

builder.Services.AddTransient<IFooService, FooService>();
builder.Services.AddTransient<BarServiceFactory>();
builder.Services.AddHostedService<App>();

IHost app = builder.Build();
app.Run();

  1. App.cs

using FoobarFactoryConsoleUI.Factories;
using FoobarFactoryConsoleUI.Interfaces;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace FoobarFactoryConsoleUI;
public class App : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly IConfiguration _config;
private readonly ILogger<App> _logger;
private readonly IFooService _fooService;
private readonly BarServiceFactory _barFactory;

  1. public App(IHostApplicationLifetime hostApplicationLifetime,
  2. IConfiguration configuration,
  3. ILogger&lt;App&gt; logger,
  4. IFooService fooService,
  5. BarServiceFactory barFactory)
  6. {
  7. _hostApplicationLifetime = hostApplicationLifetime;
  8. _config = configuration;
  9. _logger = logger;
  10. _fooService = fooService;
  11. _barFactory = barFactory;
  12. }
  13. public Task StartAsync(CancellationToken cancellationToken)
  14. {
  15. _hostApplicationLifetime.ApplicationStarted.Register(async () =&gt;
  16. {
  17. try
  18. {
  19. await Task.Yield(); // https://github.com/dotnet/runtime/issues/36063
  20. await Task.Delay(1000); // Additional delay for Microsoft.Hosting.Lifetime messages
  21. Execute();
  22. }
  23. catch (Exception ex)
  24. {
  25. _logger.LogError(ex, &quot;Unhandled exception!&quot;);
  26. }
  27. finally
  28. {
  29. _hostApplicationLifetime.StopApplication();
  30. }
  31. });
  32. return Task.CompletedTask;
  33. }
  34. public Task StopAsync(CancellationToken cancellationToken)
  35. {
  36. return Task.CompletedTask;
  37. }
  38. public void Execute()
  39. {
  40. _fooService.Foo();
  41. IBarService barService = _barFactory.CreateInstance(&quot;Hello, World!&quot;);
  42. barService.Bar();
  43. }

}

  1. BarServiceFactory.cs

using FoobarFactoryConsoleUI.Interfaces;
using FoobarFactoryConsoleUI.Services;
using Microsoft.Extensions.Logging;

namespace FoobarFactoryConsoleUI.Factories;

public class BarServiceFactory
{
private readonly ILoggerFactory _loggerFactory;

  1. public BarServiceFactory(ILoggerFactory loggerFactory)
  2. {
  3. _loggerFactory = loggerFactory;
  4. }
  5. public IBarService CreateInstance(string inputText)
  6. {
  7. ILogger&lt;BarService&gt; _barLogger = _loggerFactory.CreateLogger&lt;BarService&gt;();
  8. return new BarService(_barLogger, inputText);
  9. }

}

  1. The rest of the code is exactly the same as Method 1 above.
  2. </details>

huangapple
  • 本文由 发表于 2023年5月18日 02:21:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76275131.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定