Normally if you want to cache the result of a method call you add code into the method which explicitly checks if the result is cached and retrieves it from the cache if it is and alternatively if the result is not cached the method calls code to add the result to cache. The problem with this is that you end up having the same code repeated in numerous methods in your application and it also increases the number of and complexity of your unit tests as they have to now check that the methods are calling the caching code correctly.
It would be much better if you could control how your application performs caching via configuration with no caching code added to any of your application code. This can be achieved by using some of the featured offered by Castle Windsor, in particular facilities; which allow you to add custom functionality to any instance of a class created using the Inversion of Control functionality provided by Castle Windsor.
Below is an example configuration section which specifies that the methods GetAll, GetSale and GetSalesByCustomerAndDate in the SaleProcess class should be cached.
<castle>
<facilities>
<facility
id="cacheFacility"
type="Blog.Caching.Interception.CacheFacility, Blog.Caching.Interception" />
</facilities>
<components>
<component
id="customerProcess"
service="Blog.Caching.Interception.Example.Process.ICustomerProcess, Blog.Caching.Interception.Example"
type="Blog.Caching.Interception.Example.Process.CustomerProcess, Blog.Caching.Interception.Example"
lifestyle="transient"/>
<component
id="saleProcess"
service="Blog.Caching.Interception.Example.Process.ISaleProcess, Blog.Caching.Interception.Example"
type="Blog.Caching.Interception.Example.Process.SaleProcess, Blog.Caching.Interception.Example"
lifestyle="transient"
isCachable="true">
<cache>
<method
name="GetAll"
cacheKeyGenerator="noArgsCacheKeyGenerator"
enabled="true"
isSlidingExpiration="false"
expirationIncrement="minutes"
expirationValue="10"
priority="normal"/>
<method
name="GetSale"
cacheKeyGenerator="singleArgumentCacheKeyGenerator"
enabled="true"
isSlidingExpiration="false"
expirationIncrement="minutes"
expirationValue="10"
priority="normal"/>
<method
name="GetSalesByCustomerAndDate"
cacheKeyGenerator="saleByCustomerAndDateCacheKeyGenerator"
enabled="true"
isSlidingExpiration="false"
expirationIncrement="minutes"
expirationValue="10"
priority="normal"/>
</cache>
</component>
<!-- Caching -->
<component
id="cacheManager"
service="Blog.Caching.Interception.ICacheManager, Blog.Caching.Interception"
type="Blog.Caching.Interception.Example.Caching.EnterpriseLibraryCacheManager, Blog.Caching.Interception.Example"
lifestyle="singleton"/>
<component
id="noArgsCacheKeyGenerator"
service="Blog.Caching.Interception.ICacheKeyGenerator, Blog.Caching.Interception"
type="Blog.Caching.Interception.Example.Process.NoArgsCacheKeyGenerator, Blog.Caching.Interception.Example"
lifestyle="singleton"/>
<component
id="saleByCustomerAndDateCacheKeyGenerator"
service="Blog.Caching.Interception.ICacheKeyGenerator, Blog.Caching.Interception"
type="Blog.Caching.Interception.Example.Process.SaleByCustomerAndDateCacheKeyGenerator, Blog.Caching.Interception.Example"
lifestyle="singleton"/>
<component
id="singleArgumentCacheKeyGenerator"
service="Blog.Caching.Interception.ICacheKeyGenerator, Blog.Caching.Interception"
type="Blog.Caching.Interception.Example.Process.SingleArgumentCacheKeyGenerator, Blog.Caching.Interception.Example"
lifestyle="singleton"/>
</components>
</castle>
As you can see in the castle configuration section we have specified a cache facility which is responsible for adding caching functionality to any component. To make use of the cache facility all you have to do is simply set the “isCachable” attribute on a component to “true” and specify what methods belonging to the component are required to be cached. As you can see we have the ability to pick and choose which methods are cached and specify exactly how they are cached.
The section below details the various bits of code that make up the cache facility.
/// <summary>
/// Cache facility
/// </summary>
public class CacheFacility : AbstractFacility
{
/// <summary>
/// Overriden init method
/// </summary>
protected override void Init()
{
Kernel.AddComponent("cache.interceptor", typeof(CacheInterceptor), LifestyleType.Transient);
Kernel.AddComponent("cache.MetaInfoStore", typeof(CacheMetaInfoStore));
Kernel.ComponentModelBuilder.AddContributor(new CacheComponentInspector());
}
}
All the CacheFacility class does is wire up the different components that the facility uses, they are
- CacheInterceptor – responsible for intercepting the call to the cached method and retrieving the result from the cache
- CacheMetaInfoStore – responsible for storing the data which describes how a method’s result should be cached
- CacheComponentInspector – responsible for inspecting the components defined in the castle configuration section and finding out what needs to be cached
Below is CacheMetaInfoStore class
/// <summary>
/// Stores of cache meta info
/// </summary>
public class CacheMetaInfoStore : MarshalByRefObject
{
private static readonly string CacheKeyGeneratorAttribute = "cacheKeyGenerator";
private static readonly string EnabledAttribute = "enabled";
private static readonly string IsSlidingExpirationAttribute = "isSlidingExpiration";
private static readonly string ExpirationIncrementAttribute = "expirationIncrement";
private static readonly string ExpirationValueAttribute = "expirationValue";
private static readonly string PriorityAttribute = "priority";
private static readonly string CacheStoreAttribute = "cacheStore";
private readonly Dictionary<Type, CacheMetaInfo> typeMetaInfo = new Dictionary<Type, CacheMetaInfo>();
/// <summary>
/// Constructor
/// </summary>
public CacheMetaInfoStore() { }
/// <summary>
/// Overriden method
/// </summary>
/// <returns></returns>
public override object InitializeLifetimeService()
{
return null;
}
/// <summary>
/// Creates the meta information from the configuration file
/// </summary>
/// <param name="implementation">Type to check the cache setting for</param>
/// <param name="methods">List of cached method</param>
/// <param name="config">Config</param>
/// <returns></returns>
public CacheMetaInfo CreateMetaFromConfig(Type implementation, MethodInfo[] methods, IConfiguration config)
{
CacheMetaInfo metaInfo = GetMetaFor(implementation);
if (metaInfo == null)
{
metaInfo = new CacheMetaInfo();
}
foreach (MethodInfo method in methods)
{
string cacheKeyGenerator = config.Attributes[CacheKeyGeneratorAttribute];
bool enabled = Convert.ToBoolean(config.Attributes[EnabledAttribute]);
bool isSlidingExpiration = Convert.ToBoolean(config.Attributes[IsSlidingExpirationAttribute]);
string expirationIncrement = config.Attributes[ExpirationIncrementAttribute];
int expirationValue = Convert.ToInt32(config.Attributes[ExpirationValueAttribute]);
string priority = config.Attributes[PriorityAttribute];
string cacheStore = config.Attributes[CacheStoreAttribute];
CacheItemDataModel cacheItemData = new CacheItemDataModel
{
CacheKeyGenerator = cacheKeyGenerator,
Enabled = enabled,
IsSlidingExpiration = isSlidingExpiration,
ExpirationIncrement = expirationIncrement,
ExpirationValue = expirationValue,
Priority = priority,
CacheStore = cacheStore
};
metaInfo.Add(method, cacheItemData);
}
Register(implementation, metaInfo);
return metaInfo;
}
/// <summary>
/// Gets the meta info for the specified type
/// </summary>
/// <param name="implementation">Type to check for</param>
/// <returns>CacheMetaInfo</returns>
public CacheMetaInfo GetMetaFor(Type implementation)
{
if (!typeMetaInfo.ContainsKey((implementation)))
{
return null;
}
return typeMetaInfo[implementation];
}
/// <summary>
/// Registered a types meta info
/// </summary>
/// <param name="implementation">The type</param>
/// <param name="metaInfo">Meta info</param>
private void Register(Type implementation, CacheMetaInfo metaInfo)
{
if (typeMetaInfo.ContainsKey(implementation))
{
typeMetaInfo[implementation] = metaInfo;
}
else
{
typeMetaInfo.Add(implementation, metaInfo);
}
}
}
T he CreateMetaFromConfig method is responsible for retrieving the caching configuration specified for a set of methods and storing this data in a dictionary keyed off the type. The GetMetaFor method is responsible for retrieving the data which describes how a type’s methods are cached.
/// <summary>
/// Cache component inspector
/// </summary>
public class CacheComponentInspector : MethodMetaInspector
{
private static readonly String CacheNodeName = "cache";
private CacheMetaInfoStore metaStore;
/// <summary>
/// Tries to obtain transaction configuration based on
/// the component configuration or (if not available) check
/// for the attributes.
/// </summary>
/// <param name="kernel">The kernel.</param>
/// <param name="model">The model.</param>
public override void ProcessModel(IKernel kernel, ComponentModel model)
{
if (metaStore == null)
{
metaStore = (CacheMetaInfoStore)kernel[typeof(CacheMetaInfoStore)];
}
if (IsCachable(model.Configuration))
{
base.ProcessModel(kernel, model);
}
Validate(model, metaStore);
AddInterceptorIfIsCachable(model, metaStore);
}
/// <summary>
/// Obtains the name of the
/// node (overrides MethodMetaInspector.ObtainNodeName)
/// </summary>
/// <returns>the node name on the configuration</returns>
protected override String ObtainNodeName()
{
return CacheNodeName;
}
/// <summary>
/// Processes the meta information available on
/// the component configuration. (overrides MethodMetaInspector.ProcessMeta)
/// </summary>
/// <param name="model">The model.</param>
/// <param name="methods">The methods.</param>
/// <param name="metaModel">The meta model.</param>
protected override void ProcessMeta(ComponentModel model, MethodInfo[] methods, MethodMetaModel metaModel)
{
metaStore.CreateMetaFromConfig(model.Implementation, methods, metaModel.ConfigNode);
}
/// <summary>
/// Validates the type is OK to generate a proxy.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="store">The store.</param>
private void Validate(ComponentModel model, CacheMetaInfoStore store)
{
if (model.Service == null || model.Service.IsInterface)
{
return;
}
CacheMetaInfo meta = store.GetMetaFor(model.Implementation);
if (meta == null)
{
return;
}
//Get any non virtual methods as these cannot be intercepted
List<string> problematicMethods = meta.Methods.Where(method => !method.IsVirtual).Select(method => method.Name).ToList();
if (problematicMethods.Count != 0)
{
String[] methodNames = problematicMethods.ToArray();
String message = String.Format("The class {0} wants to use cache interception, " +
"however the methods must be marked as virtual in order to do so. Please correct " +
"the following methods: {1}", model.Implementation.FullName,
String.Join(", ", methodNames));
throw new FacilityException(message);
}
}
/// <summary>
/// Determines whether the configuration has <c>istransaction="true"</c> attribute.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <returns>
/// <c>true</c> if yes; otherwise, <c>false</c>.
/// </returns>
private bool IsCachable(IConfiguration configuration)
{
return (configuration != null && "true" == configuration.Attributes["isCachable"]);
}
/// <summary>
/// Associates the interceptor with the ComponentModel.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="store">The meta information store.</param>
private static void AddInterceptorIfIsCachable(ComponentModel model, CacheMetaInfoStore store)
{
CacheMetaInfo meta = store.GetMetaFor(model.Implementation);
if (meta == null)
{
return;
}
model.Dependencies.Add(
new DependencyModel(DependencyType.Service, null, typeof(CacheInterceptor), false));
model.Interceptors.AddFirst(new InterceptorReference(typeof(CacheInterceptor)));
}
}
The ProcessModel method is responsible for inspecting the component (defined in the castle configuration section) checking if the component is cacheable and that the methods to be cached are can be cached (only virtual methods or methods defined on an interface can be intercepted). Finally if the component is to be cached associating the CacheInterceptor to it.
/// <summary>
/// Caching interceptor
/// </summary>
public class CacheInterceptor : IInterceptor, IOnBehalfAware
{
private readonly IKernel kernel;
private readonly CacheMetaInfoStore infoStore;
private CacheMetaInfo metaInfo;
/// <summary>
/// Constructor
/// </summary>
/// <param name="kernel">IKernel instance</param>
/// <param name="infoStore">Cache meta data store</param>
[System.Diagnostics.DebuggerStepThroughAttribute()]
public CacheInterceptor(IKernel kernel, CacheMetaInfoStore infoStore)
{
this.kernel = kernel;
this.infoStore = infoStore;
}
/// <summary>
/// Retrieves the meta info the type being intercepted
/// </summary>
/// <param name="target">Object being intercepted</param>
[System.Diagnostics.DebuggerStepThroughAttribute()]
public void SetInterceptedComponentModel(ComponentModel target)
{
metaInfo = infoStore.GetMetaFor(target.Implementation);
}
/// <summary>
/// Intercepts the method and performs caching
/// </summary>
/// <param name="invocation">Invocation being intercepted</param>
[System.Diagnostics.DebuggerStepThroughAttribute()]
public void Intercept(IInvocation invocation)
{
MethodInfo methodInfo = invocation.MethodInvocationTarget;
if (methodInfo == null || !metaInfo.Contains(methodInfo))
{
//Method not cached so just call the method
invocation.Proceed();
}
else
{
bool cacheResult = true;
CacheItemDataModel cacheItemData = metaInfo.GetCacheItemDataFor(methodInfo);
if (cacheItemData.Enabled)
{
ICacheManager cacheManager = (ICacheManager)kernel[typeof(ICacheManager)];
ICacheKeyGenerator cacheKeyGenerator = (ICacheKeyGenerator)kernel[cacheItemData.CacheKeyGenerator];
string cacheKey = cacheKeyGenerator.CreateCacheKey(invocation.Method, invocation.Arguments);
object result = cacheManager.GetObjectFromCache(cacheKey, cacheItemData);
if (result == null)
{
//Call the method and get the result
invocation.Proceed();
result = invocation.ReturnValue;
if (result != null)
{
//Check the collections being cached are not empty
if (result is ICollection)
{
cacheResult = ShouldCacheCollection((ICollection)result);
}
//Check if the result should be cached
if (cacheResult)
{
cacheManager.SetObjectToCache(result, cacheKey, cacheItemData);
}
}
}
invocation.ReturnValue = result;
}
else
{
invocation.Proceed();
}
}
}
/// <summary>
/// Works out if the collection should be cached
/// </summary>
/// <param name="collection">The collection to check</param>
/// <returns>True if the collection should be cached, otherwise false</returns>
[System.Diagnostics.DebuggerStepThroughAttribute()]
private bool ShouldCacheCollection(System.Collections.ICollection collection)
{
return collection.Count > 0;
}
}
The CacheInterceptor class is responsible for controlling how the result of a method is cached. The Intercept method is called whenever the method is called it checks to see if the method should be cached if not it simply forwards the call onto the component that is being intercepted. Otherwise if the method is cached it retrieves the CacheManager and correct CacheKeyGenerator (both of which are defined in the castle configuration). It then tries to retrieve the result from the CacheManger if a result is returned the interceptor returns this value without ever invoking the component. On the other hand if no result was retrieved from the CacheManager the interceptor invokes the component and retrieves the result and stores the result in the cache.
The CacheManager described above implements the ICacheManager interface as shown below along with a concrete implementation which uses the caching functionality provided by the enterprise library; it is responsible for getting data from the cache and setting data to the cache
/// <summary>
/// Specifies the functionality provided by a cache manager
/// </summary>
public interface ICacheManager
{
/// <summary>
/// Caches the supplied item
/// </summary>
/// <param name="itemToCache">The object to cache</param>
/// <param name="cacheKey">The cache key to use</param>
/// <param name="cacheItemData">Additional information which describes how to cache the item</param>
/// <returns>True if the items was added to the cache, otherwise false</returns>
bool SetObjectToCache(object itemToCache, string cacheKey, CacheItemDataModel cacheItemData);
/// <summary>
/// Retrieves an item from the cache
/// </summary>
/// <param name="cacheKey">The cache key of the item to retrieve</param>
/// <param name="cacheItemData">Additional information which describes how to retrieve the item</param>
/// <returns>The cache item, or null if no object was cached with the specified cache key</returns>
object GetObjectFromCache(string cacheKey, CacheItemDataModel cacheItemData);
}
/// <summary>
/// Enterprise library cache manager implementation
/// </summary>
public class EnterpriseLibraryCacheManager : ICacheManager
{
/// <summary>
/// Caches the supplied item
/// </summary>
/// <param name="itemToCache">The object to cache</param>
/// <param name="cacheKey">The cache key to use</param>
/// <param name="cacheItemData">Additional information which describes how to cache the item</param>
/// <returns>True if the items was added to the cache, otherwise false</returns>
public bool SetObjectToCache(object itemToCache, string cacheKey, CacheItemDataModel cacheItemData)
{
bool objectAddedToCache = false;
CacheManager cache = GetCache(cacheItemData);
if (cache != null)
{
CacheItemPriority priority = MapCachingPriority(cacheItemData);
ICacheItemExpiration expiration = GetCacheExpiration(cacheItemData);
cache.Add(cacheKey, itemToCache, priority, null, expiration);
objectAddedToCache = true;
}
return objectAddedToCache;
}
/// <summary>
/// Retrieves an item from the cache
/// </summary>
/// <param name="cacheKey">The cache key of the item to retrieve</param>
/// <param name="cacheItemData">Additional information which describes how to retrieve the item</param>
/// <returns>The cache item, or null if no object was cached with the specified cache key</returns>
public object GetObjectFromCache(string cacheKey, CacheItemDataModel cacheItemData)
{
object result = null;
CacheManager cache = GetCache(cacheItemData);
if (cache != null)
{
result = cache.GetData(cacheKey);
}
return result;
}
/// <summary>
/// Gets the cache to use
/// </summary>
/// <param name="cacheItemData">Contains details of the cache to use</param>
/// <returns>CacheManager</returns>
private CacheManager GetCache(CacheItemDataModel cacheItemData)
{
CacheManager cache = null;
if (string.IsNullOrEmpty(cacheItemData.CacheStore))
{
cache = CacheFactory.GetCacheManager();
}
else
{
cache = CacheFactory.GetCacheManager(cacheItemData.CacheStore);
}
if (cache == null)
{
throw new Exception("Cache null");
}
return cache;
}
/// <summary>
/// Maps from a CacheItemDataModel to CacheItemPriority
/// </summary>
/// <param name="cacheItemData">Contains the cache item priority to map from</param>
/// <returns>CacheITemPriority</returns>
private CacheItemPriority MapCachingPriority(CacheItemDataModel cacheItemData)
{
CacheItemPriority to;
switch (cacheItemData.Priority.ToLower())
{
case "high":
to = CacheItemPriority.High;
break;
case "low":
to = CacheItemPriority.Low;
break;
case "normal":
to = CacheItemPriority.Normal;
break;
case "notremovable":
to = CacheItemPriority.NotRemovable;
break;
default:
to = CacheItemPriority.Normal;
break;
}
return to;
}
/// <summary>
/// Gets the ICacheItemExpiration from the CacheItemDataModel
/// </summary>
/// <param name="cacheItemData">Contains details which describe the what cache item expiration policy should be used</param>
/// <returns>ICacheItemExpiration</returns>
private ICacheItemExpiration GetCacheExpiration(CacheItemDataModel cacheItemData)
{
TimeSpan time = GetCacheTimeSpan(cacheItemData);
ICacheItemExpiration expiration;
if (cacheItemData.IsSlidingExpiration)
{
expiration = new SlidingTime(time);
}
else
{
expiration = new AbsoluteTime(time);
}
return expiration;
}
/// <summary>
/// Returns a timespan which is how long the cache
/// is valid for
/// </summary>
/// <param name="cacheItemData">Contains details of how long the cache is valid for</param>
/// <returns>Timespace</returns>
private TimeSpan GetCacheTimeSpan(CacheItemDataModel cacheItemData)
{
TimeSpan span = TimeSpan.MinValue;
switch (cacheItemData.ExpirationIncrement.ToLower())
{
case "seconds":
span = TimeSpan.FromSeconds(cacheItemData.ExpirationValue);
break;
case "minutes":
span = TimeSpan.FromMinutes(cacheItemData.ExpirationValue);
break;
case "hours":
span = TimeSpan.FromHours(cacheItemData.ExpirationValue);
break;
case "days":
span = TimeSpan.FromDays(cacheItemData.ExpirationValue);
break;
default:
break;
}
return span;
}
}
The CacheKeyGenerator implements the ICacheKeyGenerator interface; it is responsible for working out what cache key should be used for the object to cache.
/// <summary>
/// Specifies the functionality provided by
/// cache key generators
/// </summary>
public interface ICacheKeyGenerator
{
/// <summary>
/// Creates a cache key for the supplied
/// method invocation
/// </summary>
/// <param name="methodInfo">Method that is to be invoked</param>
/// <param name="arguments">Arguments that are to be passed to the method</param>
/// <returns></returns>
string CreateCacheKey(MethodInfo methodInfo, object[] arguments);
}
You will need to define a number of cache key generators to ensure that the correct unique cache key is generated in all scenarios e.g.
- When a method has no arguments
- When a method has a single argument
- When a method has a number of arguments
Below are two example cache key generators
/// <summary>
/// Generates cache keys for methods with no arguments
/// </summary>
public class NoArgsCacheKeyGenerator : ICacheKeyGenerator
{
/// <summary>
/// Creates a cache key for the supplied
/// method invocation
/// </summary>
/// <param name="methodInfo">Method that is to be invoked</param>
/// <param name="arguments">Arguments that are to be passed to the method</param>
/// <returns></returns>
public string CreateCacheKey(MethodInfo methodInfo, object[] arguments)
{
return methodInfo.Name + methodInfo.ReturnType.Name;
}
}
/// <summary>
/// Generates cache keys for methods with a single argument
/// </summary>
public class SingleArgumentCacheKeyGenerator : ICacheKeyGenerator
{
/// <summary>
/// Creates a cache key for the supplied
/// method invocation
/// </summary>
/// <param name="methodInfo">Method that is to be invoked</param>
/// <param name="arguments">Arguments that are to be passed to the method</param>
/// <returns></returns>
public string CreateCacheKey(MethodInfo methodInfo, object[] arguments)
{
return methodInfo.Name + methodInfo.ReturnType.Name + arguments[0].ToString();
}
}
The next example shows the code from a simple console application which pulls all the above together
class Program
{
private static readonly IWindsorContainer container = new WindsorContainer(new XmlInterpreter());
static void Main(string[] args)
{
Console.WriteLine("Press any key to start example");
Console.ReadKey();
ISaleProcess saleProcess = CreateProcess<ISaleProcess>();
//GetAll cached
List<SaleModel> result1 = saleProcess.GetAll();
List<SaleModel> result2 = saleProcess.GetAll();
//References should be same as result cached
Console.WriteLine("Result of ReferenceEquals(result1, result2) - " + ReferenceEquals(result1, result2).ToString());
//GetSale cached
SaleModel result3 = saleProcess.GetSale(5);
SaleModel result4 = saleProcess.GetSale(5);
//References should be same as result cached
Console.WriteLine("Result of ReferenceEquals(result3, result4) - " + ReferenceEquals(result3, result4).ToString());
SaleModel result5 = saleProcess.GetSale(6);
//References should not be same as result cached but different argument passed into method
Console.WriteLine("Result of ReferenceEquals(result4, result5) - " + ReferenceEquals(result4, result5).ToString());
//GetSaleByDate not cached
DateTime now = DateTime.Now;
List<SaleModel> result6 = saleProcess.GetSaleByDate(now);
List<SaleModel> result7 = saleProcess.GetSaleByDate(now);
//References should not be same as results are not cached
Console.WriteLine("Result of ReferenceEquals(result6, result7) - " + ReferenceEquals(result6, result7).ToString());
Console.WriteLine("Press any key to end example");
Console.ReadKey();
}
/// <summary>
/// Creates a process
/// </summary>
/// <typeparam name="T">Type of the process to create</typeparam>
/// <returns>Process instance</returns>
private static T CreateProcess<T>()
{
T process = container.Resolve<T>();
return process;
}
}
And below is the ISaleProcess interface and SaleProcess class, as you can see there is no caching code in the implementation therefore we have successfully extracted the caching logic from the core application code.
public interface ISaleProcess
{
List<SaleModel> GetAll();
SaleModel GetSale(int id);
List<SaleModel> GetSalesByCustomerAndDate(string firstname, string surname, DateTime saleDate);
List<SaleModel> GetSaleByDate(DateTime saleDate);
}
public class SaleProcess : ISaleProcess
{
public List<SaleModel> GetAll()
{
return new List<SaleModel> {new SaleModel {Id = 1, Date = DateTime.Now}};
}
public SaleModel GetSale(int id)
{
return new SaleModel {Id = id, Date = DateTime.Now};
}
public List<SaleModel> GetSalesByCustomerAndDate(string firstname, string surname, DateTime saleDate)
{
return new List<SaleModel>
{
new SaleModel{Id = 10, Customer = new CustomerModel{Firstname = firstname, Surname = surname}, Date = saleDate}
};
}
public List<SaleModel> GetSaleByDate(DateTime saleDate)
{
return new List<SaleModel>
{
new SaleModel{Id = 20, Date = saleDate}
};
}
}