In console application with dependency injection with Options pattern I'm trying to load user input. How can I make "factory like" resolving of
IOptions<ArgSettings>
to load input arguments. If required input arguments are not provided (null or not valid) then use default settings from appsettings.json?
public class ArgSettings
{
public string Name { get; set; }
public string Region { get; set; }
}
public interface IService
{
void DoSomthing();
}
public class MyService: IService
{
private readonly ArgSettings _argSettings;
public MyService(IOptions<ArgSettings> cfg)
{
_argSettings = cfg.Value;
}
public void DoSomthing()
{
}
}
public class Program
{
static void Main(string[] args)
{
ArgSettings argsSettings = BindArgsSettings(Environment.GetCommandLineArgs());
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
var configurationRoot = context.Configuration;
services.Configure<ArgSettings>(
configurationRoot.GetSection("Defaults:Args"));
services.AddTransient<IService, MyService>();
}).Build();
}
}
default config section in appsettings.json:
...
"Defaults": {
"Args": {
"Name": "John",
"Region": "USA"
}
}
...
First, when you use IOptions.. you have to use the ".Value"
below code is from : https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
Or more generically (less ambiguous):
public class MyCoolClass
{
private readonly MyWhateverSettings _settings;
public MyCoolClass(IOptions<MyWhateverSettings> options)
{
_settings = options.Value;
}
(back to your code)
While I see this code above:
BindArgsSettings(Environment.GetCommandLineArgs());
I don't see what you are doing with the "result of that call".
Ok, here is a
Unmodified Microsoft Sample: (from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0
)
using ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
var app = builder.Build();
(the above being used as a sanity check)
What I think you want:
/* has the possibility to be null, if the user "passes" on customizing it */
ArgSettings myArgsSettings = BindArgsSettings(Environment.GetCommandLineArgs());
if (null == myArgsSettings)
{
/* user "passed up" doing it manually, so go get the "Defaults" */
myArgsSettings = configurationRoot.GetSection("Defaults:Args");
}
/* now DI inject */
builder.Services.Configure<ArgSettings>(myArgsSettings);
Aka, you are doing something with the result (from 'BindArgsSettings(Environment.GetCommandLineArgs()') .. aka, what has hydrated the 'myArgsSettings' variable. (the first hydration of myArgsSettings that is)
Or below is a "less ambiguous" implementation:
/* has the possibility to be null, if the user "passes" on customizing it */
ArgSettings userProvidedArgsSettings = BindArgsSettings(Environment.GetCommandLineArgs());
ArgSettings fromJsonArgSettings = null;
if (null == userProvidedArgsSettings)
{
/* user "passed up" doing it manually, so go get the "Defaults" */
fromJsonArgSettings = configurationRoot.GetSection("Defaults:Args");
}
if (null == userProvidedArgsSettings && null == fromJsonArgSettings)
{
throw new ArgumentNullException("Guh, Both UserProvider AND fromJson are null.", (Exception) null);
}
/* now DI inject */
if (null != userProvidedArgsSettings)
{
builder.Services.Configure<ArgSettings>(userProvidedArgsSettings);
}
if (null != fromJsonArgSettings)
{
builder.Services.Configure<ArgSettings>(fromJsonArgSettings);
}
Related
Objects are rendered as strings, (name of the object), in Application Insights custom dimensions when passed as arguments to ilogger. The actual values are not shown.
Register Application Insights
services.AddApplicationInsightsTelemetry();
New log
public class HealthController : ControllerBase
{
private readonly ILogger<HealthController> _logger;
public HealthController(ILogger<HealthController> logger)
{
_logger = logger;
}
[HttpGet]
public IActionResult Get()
{
var health = new HealthViewModel()
{
ok = false
};
_logger.LogInformation("Hlep me pls {health}", health);
return Ok(health);
}
}
Result
I do not want to this this for every log:
var health = new HealthViewModel()
{
ok = false
};
_logger.LogInformation("Hlep me pls {health}", JsonConvert.SerializeObject(health));
I tried creating a middleware for application insights but the value is still the name of the object..
Why are arguments not rendered as json?
Edit
It seems like
var health = new
{
ok = false
};
_logger.LogInformation("HEJ2 {health}", health);
works but not
var health = new HealthViewModel
{
ok = false
};
_logger.LogInformation("HEJ2 {health}", health);
Not supported
Quote from https://github.com/microsoft/ApplicationInsights-dotnet/issues/1722
I think you're expecting too much of the logger. It doesn't know about JSON format, it just calls Convert.ToString on properties
Convert.ToString typically calls ToString() and the default ToString implementation for new classes is simply to return the type name
What you can do
Use ToJson() on objects logged to ILogger and create a middleware for application insights and modify the name of the log and the custom dimensions.
Middleware
public class ProcessApiTraceFilter : ITelemetryProcessor
{
private ITelemetryProcessor Next { get; set; }
private readonly IIdentity _identity;
private readonly IHostEnvironment _hostEnvironment;
public ProcessApiTraceFilter(ITelemetryProcessor next, IHostEnvironment hostEnvironment, IIdentity identity)
{
Next = next;
_identity = identity;
_hostEnvironment = hostEnvironment;
}
public void Process(ITelemetry item)
{
item.Process(_hostEnvironment, _identity);
Next.Process(item);
}
}
Implementation
public static class ApplicationInsightsExtensions
{
public static void Process(this ITelemetry item, IHostEnvironment hostEnvironment, IIdentity identity)
{
if (item is TraceTelemetry)
{
var traceTelemetry = item as TraceTelemetry;
var originalMessage = traceTelemetry.Properties.FirstOrDefault(x => x.Key == "{OriginalFormat}");
if (!string.IsNullOrEmpty(originalMessage.Key))
{
var reg = new Regex("{([A-z]*)*}", RegexOptions.Compiled);
var match = reg.Matches(originalMessage.Value);
var formattedMessage = originalMessage.Value;
foreach (Match arg in match)
{
var parameterName = arg.Value.Replace("{", "").Replace("}", "");
var parameterValue = traceTelemetry.Properties.FirstOrDefault(x => x.Key == parameterName);
formattedMessage = formattedMessage.Replace(arg.Value, "");
}
traceTelemetry.Message = formattedMessage.Trim();
}
if (identity != null)
{
var isAuthenticated = identity.IsAuthenticated();
const string customerKey = "customer";
if (isAuthenticated && !traceTelemetry.Properties.ContainsKey(customerKey))
{
var customer = identity.Customer();
if (customer != null)
{
traceTelemetry.Properties.Add(customerKey, customer.ToJson());
}
}
var request = identity.Request();
const string requestKey = "request";
if (request != null && !traceTelemetry.Properties.ContainsKey(requestKey))
{
traceTelemetry.Properties.Add(requestKey, request.ToJson());
}
}
var applicationNameKey = "applicationName";
if (hostEnvironment != null && !string.IsNullOrEmpty(hostEnvironment.ApplicationName) && !traceTelemetry.Properties.ContainsKey(applicationNameKey))
{
traceTelemetry.Properties.Add(applicationNameKey, hostEnvironment.ApplicationName);
}
}
}
}
Register application insights and middleware in startup
services.AddApplicationInsightsTelemetry();
services.AddApplicationInsightsTelemetryProcessor<ProcessApiTraceFilter>();
ToJson
public static class ObjectExtensions
{
private static readonly string Null = "null";
private static readonly string Exception = "Could not serialize object to json";
public static string ToJson(this object value, Formatting formatting = Formatting.None)
{
if (value == null) return Null;
try
{
string json = JsonConvert.SerializeObject(value, formatting);
return json;
}
catch (Exception ex)
{
return $"{Exception} - {ex?.Message}";
}
}
}
Log
//Log object? _smtpAppSettings.ToJson()
_logger.LogInformation("Email sent {to} {from} {subject}", to, _smtpAppSettings.From, subject)
Result
from your custom dimensions i can see that it`s not considering the health obj param as an extra data
_logger.LogInformation("Hlep me pls {health}", health);
trying using the jsonConverter within the string itself.
_logger.LogInformation($"Hlep me pls {JsonConvert.SerializeObject(health)}");
We have some legacy app assume that we can not change that SiteSettings class because complete project coding thousands of line will disturb. so we want to solve problem using DI. I created POC app here you can see in global asax there is comment //HOW CAN I PASS TenantId HERE so it will be same for this complete httprequest life.
LegacyCode:
public class OrderController
{
public static string CompleteOrder()
{
return SiteSettings.Instance.DefaultTimeZone();
}
}
public class SiteSettings
{
public ITenantSettings TenantSettings { get; set; }
private static SiteSettings _instance;
private SiteSettings() { }
public static SiteSettings Instance => _instance ?? (_instance = new SiteSettings());
public string DefaultTimeZone()
{
return TenantSettings.DefaultTimeZone();
}
}
New Classes for Injection
public interface ITenantSettings
{
string DefaultTimeZone();
}
public class TenantSettings : ITenantSettings
{
private readonly int _tenantId;
public TenantSettings(int tenantId)
{
_tenantId = tenantId;
}
public string DefaultTimeZone()
{
return "USA Time For Tenant ID " + _tenantId.ToString();
}
}
Global ASAX
public class Global : HttpApplication, IContainerProviderAccessor
{
// Provider that holds the application container.
static IContainerProvider _containerProvider;
// Instance property that will be used by Autofac HttpModules
// to resolve and inject dependencies.
public IContainerProvider ContainerProvider => _containerProvider;
protected void Application_Start(object sender, EventArgs e)
{
// Build up your application container and register your dependencies.
var builder = new ContainerBuilder();
builder.RegisterType<TenantSettings>().As<ITenantSettings>().InstancePerRequest();
_containerProvider = new ContainerProvider(builder.Build());
}
protected void Application_BeginRequest(object sender, EventArgs e)
{
int id = 0;
int.TryParse(HttpContext.Current.Request.QueryString["id"], out id);
var cpa = (IContainerProviderAccessor)HttpContext.Current.ApplicationInstance;
var cp = cpa.ContainerProvider;
cp.RequestLifetime.InjectProperties(SiteSettings.Instance);
//HOW CAN I PASS TENANTID HERE so it will be same for this complete httprequest life.
}
}
Default ASPX
public partial class Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.Write(OrderController.CompleteOrder());
}
}
Error :
None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'CoreLibrary.Tenants.TenantSettings' can be invoked with the available services and parameters:
Cannot resolve parameter 'Int32 tenantId' of constructor 'Void .ctor(Int32)'.
You can use WithParameter, in this instance I would suggest the ResolvedParameter:
builder.RegisterType<TenantSettings>()
.As<ITenantSettings>()
.InstancePerRequest()
.WithParameter(
new ResolvedParameter(
(pi, ctx) => pi.ParameterType == typeof(int) && pi.Name == "tenantId",
(pi, ctx) => int.Parse(HttpContext.Current.Request.QueryString["id"])));
In reality you will need something a little more resilient than int.Parse(HttpContext.Current.Request.QueryString["id"]) but this gives you a flavour of a solution
Update
We need to remove the line _instance ?? (_instance = new SiteSettings()) if we are to inject the dependencies. In my example SiteSettings now has a static Initialise method, and this method is the used to construct the value of SiteSettings.Instance.
Currently we are only interested in injecting ITenantSettings and as we want ITenantSettings to have a lesser lifetime scope (per request) than the scope of SiteSettings (singleton) we should inject a delegate (Func<ITenantSettings>).
public class SiteSettings
{
private static SiteSettings _instance;
private Func<ITenantSettings> _tenantSettingsFactory;
private SiteSettings(Func<ITenantSettings> tenantSettingsFactory)
{
_tenantSettingsFactory = tenantSettingsFactory;
}
public static void Initialise(Func<ITenantSettings> tenantSettingsFactory)
{
_instance = new SiteSettings(tenantSettingsFactory);
}
public ITenantSettings TenantSettings { get { return _tenantSettingsFactory(); } }
public static SiteSettings Instance
{
get {
if (_instance == null) throw new InvalidOperationException();
return _instance;
}
}
public string DefaultTimeZone()
{
return TenantSettings.DefaultTimeZone();
}
}
Here's a test that demonstrates what you are asking:
[Fact]
public void Demonstrate_TenantSettingsFactory_AlwaysResolvesCurrentTenantId()
{
int tenantId = 0;
var builder = new ContainerBuilder();
builder.RegisterType<TenantSettings>()
.As<ITenantSettings>()
.WithParameter(
new ResolvedParameter(
(pi, ctx) => pi.ParameterType == typeof(int) && pi.Name == "tenantId",
(pi, ctx) => tenantId));
var container = builder.Build();
SiteSettings.Initialise(container.Resolve<ITenantSettings>);
tenantId = 1;
Assert.Equal("USA Time For Tenant ID 1", SiteSettings.Instance.DefaultTimeZone());
tenantId = 2;
Assert.Equal("USA Time For Tenant ID 2", SiteSettings.Instance.DefaultTimeZone());
}
Note I removed InstancePerRequest and HttpContext.Current as I am using a unit test project.
I want to replace existing registered instances in Autofac with new ones in ASP.NET MVC application in runtime. Registrations are keyed as I work with collections of instances of different subtype, though it seems to be irrelevant to my issue.
Initial registration on application startup
foreach (var instance in instances)
{
builder.RegisterInstance(instance).Keyed<IInstance>(InstanceType.A);
}
IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
Further on, in a controller method I do the following: dispose old instances, obtain new ones, create a new builder, reregister existing components and also register new instances, then update Autofac's ComponentContext
//...dispose old instances, obtain new instances
var builder = new ContainerBuilder();
foreach (var c in _componentContext.ComponentRegistry.Registrations)
{
builder.RegisterComponent(c);
}
foreach (var instance in newInstances)
{
builder.RegisterInstance(instance).Keyed<IInstance>(InstanceType.A);
}
builder.Update(_componentContext.ComponentRegistry);
Next time I enter the controller method, in controller constructor the old instances are resolved as IIndex<InstanceType, IInstance[]>, not the new ones. What am I doing wrong?
Your code doesn't work because componentContext is the context for the current scope and not the global scope. You can look at this .NetFiddle to show some code illustrating the problem : https://dotnetfiddle.net/GNvOL4
If you really want to replace instance it will be simpler to use a provider :
class Program
{
static void Main(string[] args)
{
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterInstance(new FooProvider(new Foo("a")))
.As<FooProvider>();
builder.Register(c => c.Resolve<FooProvider>().Value)
.ExternallyOwned()
.Keyed<Foo>(1);
IContainer container = builder.Build();
using (ILifetimeScope scope = container.BeginLifetimeScope())
{
Do(scope);
}
using (ILifetimeScope scope = container.BeginLifetimeScope())
{
IComponentContext context = scope.Resolve<IComponentContext>();
container.Resolve<FooProvider>().Value = new Foo("b");
Do(scope);
}
using (ILifetimeScope scope = container.BeginLifetimeScope())
{
Do(scope);
}
}
static void Do(ILifetimeScope scope)
{
IIndex<Int32, Foo> index = scope.Resolve<IIndex<Int32, Foo>>();
Foo foo = index[1];
Console.WriteLine(foo.Value);
}
}
public class FooProvider
{
public FooProvider(Foo value)
{
this._value = value;
}
private volatile Foo _value;
public Foo Value
{
get
{
return this._value;
}
}
public void ChangeValue(Foo value)
{
if(value == null)
{
throw new ArgumentNullException("value");
}
if(value == this._value)
{
return;
}
Foo oldValue = this._value;
this._value = value;
oldValue.Dispose();
}
public void Dispose()
{
this._value.Dispose();
}
}
public class Foo : IDisposable
{
public Foo(String value)
{
this._value = value;
}
private readonly String _value;
public String Value
{
get
{
return this._value;
}
}
public void Dispose()
{
// do things
}
}
We're using domain to customize how our application behaves. I'll illustrate it on example:
// default behavior
public class CoreService : IService {
public virtual string Hello { get { return "Hello"; } }
public virtual string FavouriteDrink { get { return "Water"; } }
}
// german.site.com
public class GermanService : CoreService {
public override string Hello { get { return "Gutten tag"; } }
public override string FavouriteDrink { get { return "Beer"; } }
}
// usa.site.com
public class UsaService : CoreService {
public override string FavouriteDrink { get { return "Cofee"; } }
}
Services are bootstrapped as follow:
var container = new UnityContainer();
container.RegisterType<IService, CoreService>();
container.RegisterType<IService, GermanService>("german.site.com");
container.RegisterType<IService, UsaService>("usa.site.com");
I use Unity to bootstrap mvc controllers. IE:
public class HomeController : Controller {
private IService m_Service;
// contructor dependency injection magic - this resolves into "CoreService"
public HomeController([Dependency]IService service) {
if (service == null) {
throw new ArgumentNullException("service");
}
m_Service = service;
}
}
Is there a way how to change unity resolution so it'll take domain into account ? Right now I ended up with
public class HomeController : Controller {
private IService m_Service;
// contructor dependency injection magic - a lot less magical
public HomeController() {
m_Service = DomainServiceLocator.Retrieve<IService>();
}
}
Support classes:
public static class DomainServiceLocator {
private static UnityContainerAdapter adapter;
public static T Retrieve<T>() {
string domain = HttpContext.Current.Request.Url.Host;
if (adapter.IsServiceRegistered(typeof(T), domain)) {
return adapter.Resolve<T>(domain);
}
return adapter.Resolve<T>();
}
}
public class QueryableContainerExtension : UnityContainerExtension {
private List<RegisterInstanceEventArgs> registeredInstances = new List<RegisterInstanceEventArgs>();
private List<RegisterEventArgs> registeredTypes = new List<RegisterEventArgs>();
protected override void Initialize() {
this.Context.Registering += (sender, e) => { this.registeredTypes.Add(e); };
this.Context.RegisteringInstance += (sender, e) => { this.registeredInstances.Add(e); };
}
public bool IsServiceRegistered(Type service, string name) {
return registeredTypes.FirstOrDefault(e => e.TypeFrom == service && e.Name == name) != null
|| registeredInstances.FirstOrDefault(e => e.RegisteredType == service && e.Name == name) != null;
}
}
public class UnityContainerAdapter {
private readonly QueryableContainerExtension queryableContainerExtension;
private readonly IUnityContainer unityContainer;
public UnityContainerAdapter()
: this(new UnityContainer()) {
}
public UnityContainerAdapter(IUnityContainer unityContainer) {
this.unityContainer = unityContainer;
// adding extensions to unity container
this.queryableContainerExtension = new QueryableContainerExtension();
unityContainer.AddExtension(this.queryableContainerExtension);
}
public T Resolve<T>(string name) {
return unityContainer.Resolve<T>(name);
}
public T Resolve<T>() {
return unityContainer.Resolve<T>();
}
public bool IsServiceRegistered(Type service, string name) {
return this.queryableContainerExtension.IsServiceRegistered(service, name);
}
}
I like to use an injection factory in these scenarios when resolving something at runtime. Essentially you're resolving your type via the domain name:
So in your composition root you could register like this:
container.RegisterType<Func<string, IService>>
(
new InjectionFactory(c => new Func<string, IService>(name => c.Resolve<IService>(name)))
);
Then in your HomeController you can inject the delegate
public class HomeController
{
private readonly Func<string,IService> _serviceFactory;
public HomeController(Func<string, IService> serviceFactory)
{
if(serviceFactory==null)
throw new ArgumentNullException("serviceFactory");
this._serviceFactory= serviceFactory;
}
public void DoSomethingWithTheService()
{
var domain = this.HttpContext.Uri.Host;
var service = this._serviceFactory(domain);
var greeting = service.Hello;
}
}
```
This is then still unit testable and you have not leaked the DI contain implementation outside of "composition root".
Also.. should CoreService be abstract to avoid direct instantiation of it?
Below is the solution I ended up with - it is based on #Spencer idea. I've created a factory, default implementation to the factory has a reference to DI container itself (IUnityContainer in my case), so it can perform the resolution based on domain once it is asked to. It is also more "modern friendly" since in current generation of ASP.NET (ASP.NET CORE) there is no such thing as magic singleton providing current HttpContext and DI is hard coded into the framework.
public interface IFactory<T>
{
T Retrieve(string domain);
}
internal sealed class Factory<T> : IFactory<T>
{
private readonly IUnityContainer _container;
public Factory(IUnityContainer container)
{
_container = container;
}
public T Resolve(string domain)
{
// this is actually more complex - we have chain inheritance here
// for simplicity assume service is either registered for given
// domain or it throws an error
return _container.Resolve<T>(domain);
}
}
// bootstrapper
var container = new UnityContainer();
container.RegisterType<IService, CoreService>();
container.RegisterType<IService, GermanService>("german.site.com");
container.RegisterType<IService, UsaService>("usa.site.com");
container.RegisterInstance<IFactory<IService>>(new Factory<IService>(container));
And the home controller looks like
public class HomeController : Controller {
private IFactory<IService> m_Factory;
public HomeController(IFactory<IService> factory) {
m_Factory = factory;
}
private void FooBar() {
var service = m_Factory.Retrieve(this.HttpContext.Uri.Host);
var hello = service.Hello;
}
}
Its also a worth mentioning that - as I'm lazy - I've build a system of decorative attributes like
[Domain("german.site.com")]
public class GermanService : IService { ... }
[DomainRoot]
public class CoreService : IService { ... }
[Domain("usa.site.com")]
public class UsaService : CoreService { ... }
So the bootstrapping is done automatically across all types in given assembly. But that part is a bit lengthy - if anyone is interested I can post it on github.
I am working on WPF application.
I use StructureMap to inject dependencies.
There are some service layer classes exist that they give parameter from constructor.
The value that I pass to constructor will change run time.
Presentation layer's classes use services to present data for user. Whenever value has changed I inject service again with new value. But active instance of presentation layer returns previous value.
I've prepared simple example for better understanding.
// static class that keeps some value
public class ValueKeeper
{
public static string Value { get; set; }
}
public interface IService
{
string Value { get; set; }
}
// Service layer class
public class Service : IService
{
// default constructor
public Service(string value)
{
Value = value;
}
#region IService Members
public string Value { get; set; }
#endregion
}
public class Program
{
private readonly IService _service;
//injecting service class
public Program(IService service)
{
_service = service;
}
// structuremap configuration
private static void Config()
{
ObjectFactory.Initialize(x => x.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();
x.For<IService>().CacheBy(InstanceScope.Hybrid).Use(() =>
{
var service = new Service("value1");
return service;
});
}));
}
// structuremap configuration after value changed.
private static void ReConfig()
{
ObjectFactory.Configure(x => x.Scan(scanner =>
{
x.For<IService>().CacheBy(InstanceScope.Hybrid).Use(() =>
{
var service =new Service(ValueKeeper.Value);
return service;
});
}));
}
private string PresentationMethod()
{
return _service.Value;
}
private static void Main(string[] args)
{
Config(); // Firtst time injecting dependencies
var prog = ObjectFactory.GetInstance<Program>();
Console.WriteLine(prog.PresentationMethod()); // returns "value1"
ValueKeeper.Value = "value 2"; //changing static property
ReConfig(); // reconfig service class with new property
Console.WriteLine(prog.PresentationMethod()); // it returns value1 but I expect value2 .
Console.ReadKey();
}
}
Real application contains many presentation and service classes.
How can I change live service instances with new object and value ?
Update :
I saw this link. It seems by using Setter Injection it's possible to change existing object.
Is setter injection my solution ?
You could use the strategy pattern to easily keep track of and switch between instances of the same interface at runtime. Here is a quick example:
var container = new Container(x => x.Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.AddAllTypesOf<IDiscountCalculator>();
}));
var strategy = container.GetInstance<IDiscountStrategy>();
Console.WriteLine(strategy.GetDiscount("Regular", 10)); // 0
Console.WriteLine(strategy.GetDiscount("Normal", 10)); // 1
Console.WriteLine(strategy.GetDiscount("Special", 10)); // 5
which depends on the following types:
public interface IDiscountStrategy
{
decimal GetDiscount(string userType, decimal orderTotal);
}
public class DiscountStrategy : IDiscountStrategy
{
private readonly IDiscountCalculator[] _discountCalculators;
public DiscountStrategy(IDiscountCalculator[] discountCalculators)
{
_discountCalculators = discountCalculators;
}
public decimal GetDiscount(string userType, decimal orderTotal)
{
var calculator = _discountCalculators.FirstOrDefault(x => x.AppliesTo(userType));
if (calculator == null) return 0;
return calculator.CalculateDiscount(orderTotal);
}
}
public interface IDiscountCalculator
{
bool AppliesTo(string userType);
decimal CalculateDiscount(decimal orderTotal);
}
public class NormalUserDiscountCalculator : IDiscountCalculator
{
public bool AppliesTo(string userType)
{
return userType == "Normal";
}
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.1m;
}
}
public class SpecialUserDiscountCalculator : IDiscountCalculator
{
public bool AppliesTo(string userType)
{
return userType == "Special";
}
public decimal CalculateDiscount(decimal orderTotal)
{
return orderTotal * 0.5m;
}
}
Or, if you have short lived dependencies that you want to dispose of right away, you should inject an abstract factory to create them on demand.
public ISomeObjectFactory
{
ISomeObject Create();
void Release(ISomeObject someObject);
}
public class SomeObjectFactory
: ISomeObjectFactory
{
//private readonly IAclModule aclModule;
// Inject dependencies at application startup here
//public SiteMapPluginProviderFactory(
// IAclModule aclModule
// )
//{
// if (aclModule == null)
// throw new ArgumentNullException("aclModule");
//
// this.aclModule = aclModule;
//}
public ISomeObject Create(IState state)
{
return new SomeObject(state);
// return new SomeObject(state, this.aclModule);
}
pubic void Release(ISomeObject someObject)
{
var disposable = someObject as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
And then use like:
public class Consumer : IConsumer
{
private readonly ISomeObjectFactory someObjectFactory;
public Consumer(ISomeObjectFactory someObjectFactory)
{
if (someObjectFactory == null)
throw new ArgumentNullException("someObjectFactory");
this.someObjectFactory = someObjectFactory;
}
public void DoSomething(IState state)
{
var instance = this.someObjectFactory.Create(state);
try
{
// Use the instance here.
}
finally
{
this.someObjectFactory.Release(instance);
}
}
}
Although not shown here, the factory could switch between different classes if needed, or you could pass a different dependency (the IState in this example) to the same type of class when it is created.
Service Locator is Anti-Pattern and should be avoided in all but the rarest of circumstances.