As most of you know, IoC can reduce coupling within our applications. CSLA has many new features that support this. For the purposes of this blog post I'm using CSLA version 4.5.4.
How do we get started? I'll start with creating the interfaces that we will use. The five most common stereotypes in CSLA are BusinessBase, BusinessListBase, ReadOnlyBase, ReadOnlyListBase and CommandBase. With each of these we can make an interface for our concrete classes that in turn inherits from the appropriate CSLA interfaces for these types:
A sample interface for BusinessBase:
1: public interface ITestBusinessBase : Csla.IBusinessBase
2: {
3: int Id { get; }
4: string Name { get; set; }
5: void SomeMethod(string someParameter);
6: }
A sample interface for BusinessListBase and a leaf:
1: public interface ITestBusinessBaseLeaf : Csla.IBusinessBase
2: {
3: int Id { get; }
4: string Name { get; set; }
5: }
6:
7: public interface ITestBusinessCollectionBase : Csla.IBusinessListBase<ITestBusinessBaseLeaf>
8: {
9: }
A sample interface for ReadOnlyBase:
1: public interface ITestReadOnlyBase : Csla.IReadOnlyBase
2: {
3: int Id { get; }
4: string Name { get; }
5: }
A sample interface for ReadOnlyListBase:
1: public interface ITestreadOnlyBaseLeaf : Csla.IReadOnlyBase
2: {
3: int Id { get; }
4: string Name { get; set; }
5: }
6:
7: public interface ITestReadOnlyListBase : Csla.IReadOnlyListBase<ITestreadOnlyBaseLeaf>
8: {
9: }
A sample interface for CommandBase:
1: public interface ITestCommandBase : Csla.ICommandBase
2: {
3: bool SomeResult();
4: }
One thing to notice here is there are no static methods and the CSLA samples generally use static factory methods. Why is this? Unfortunately static methods are not able to be added to an interface in C# so if we still use factory methods we need to be aware that any code that uses them may be tied to the concrete class that defines them. This may, or may not, be OK depending on what we are trying to do. It is important to note that the static factory method could still use an IoC container to resolve out the object factory and instance that it is creating.
Ok, so what's next? I use an IoC container, for the purposes of this posting I'll use Autofac. Generally in our production code we are going to use the same IoC container for all operations. I use a Service Locator pattern for this. There are some schools of thought that declare this pattern to be somehow wrong but I don't subscribe to that philosophy. Generally if you use ASP MVC you are already using service locators to resolve out the routes and to bind your post backs to your models. I'm not going to get too far into why the Service Locator pattern may in some cases be appropriate but suffice it to say if you are using CSLA, you will need to buy into the idea that Service Locators are at least not harmful.
The following static class gives me program wide reference to my IoC container. At some point I need to set the Container property for use:
1: public static class IoC
2: {
3: public static Autofac.IContainer Container { get; set; }
4: }
Another CSLA component that we may take a dependency on is the DataPortal itself. I generally abstract this out behind my own implementation using CSLA's IDataPortal<T> interface. For the purpose of this post, I created an IObjectFactory interface to also expose out the "child" DataPortal methods that are not part of the IDataPortal<T> interface:
1: public interface IObjectFactory<T> : IDataPortal<T>
2: {
3: TC CreateChild<TC>();
4: TC CreateChild<TC>(params object[] parameters);
5: TC FetchChild<TC>();
6: TC FetchChild<TC>(params object[] parameters);
7: void UpdateChild(object child);
8: void UpdateChild(object child, params object[] parameters);
9: }
The concrete implementation of the ObjectFactory class would be as so:
1: public sealed class ObjectFactory<T> : Common.Interfaces.IObjectFactory<T> where T : class, IMobileObject
2: {
3: public void BeginCreate(object criteria, object userState)
4: {
5: DataPortal.BeginCreate(criteria, CreateCompleted, userState);
6: }
7:
8: public void BeginCreate(object criteria)
9: {
10: DataPortal.BeginCreate(criteria, CreateCompleted);
11: }
12:
13: public void BeginCreate()
14: {
15: DataPortal.BeginCreate(CreateCompleted);
16: }
17:
18: public void BeginDelete(object criteria, object userState)
19: {
20: DataPortal.BeginDelete(criteria, DeleteCompleted, userState);
21: }
22:
23: public void BeginDelete(object criteria)
24: {
25: DataPortal.BeginDelete(criteria, DeleteCompleted);
26: }
27:
28: public void BeginExecute(T command, object userState)
29: {
30: DataPortal.BeginExecute(command, ExecuteCompleted, userState);
31: }
32:
33: public void BeginExecute(T command)
34: {
35: DataPortal.BeginExecute(command, ExecuteCompleted);
36: }
37:
38: public void BeginFetch(object criteria, object userState)
39: {
40: DataPortal.BeginFetch(criteria, FetchCompleted, userState);
41: }
42:
43: public void BeginFetch(object criteria)
44: {
45: DataPortal.BeginFetch(criteria, FetchCompleted);
46: }
47:
48: public void BeginFetch()
49: {
50: DataPortal.BeginFetch(FetchCompleted);
51: }
52:
53: public void BeginUpdate(T obj, object userState)
54: {
55: DataPortal.BeginUpdate(obj, UpdateCompleted, userState);
56: }
57:
58: public void BeginUpdate(T obj)
59: {
60: DataPortal.BeginUpdate(obj, UpdateCompleted);
61: }
62:
63: public T Create()
64: {
65: return DataPortal.Create<T>();
66: }
67:
68: public TC CreateChild<TC>()
69: {
70: return DataPortal.CreateChild<TC>();
71: }
72:
73: public TC CreateChild<TC>(params object[] parameters)
74: {
75: return DataPortal.CreateChild<TC>(parameters);
76: }
77:
78: public T Create(object criteria)
79: {
80: return DataPortal.Create<T>(criteria);
81: }
82:
83: public async Task<T> CreateAsync(object criteria)
84: {
85: return await DataPortal.CreateAsync<T>(criteria);
86: }
87:
88: public async Task<T> CreateAsync()
89: {
90: return await DataPortal.CreateAsync<T>();
91: }
92:
93: public event EventHandler<DataPortalResult<T>> CreateCompleted;
94:
95: public void Delete(object criteria)
96: {
97: DataPortal.Delete<T>(criteria);
98: }
99:
100: public Task DeleteAsync(object criteria)
101: {
102: return DataPortal.DeleteAsync<T>(criteria);
103: }
104:
105: public event EventHandler<DataPortalResult<T>> DeleteCompleted;
106:
107: public T Execute(T obj)
108: {
109: return DataPortal.Execute<T>(obj);
110: }
111:
112: public async Task<T> ExecuteAsync(T command)
113: {
114: return await DataPortal.ExecuteAsync<T>(command);
115: }
116:
117: public event EventHandler<DataPortalResult<T>> ExecuteCompleted;
118:
119: public T Fetch()
120: {
121: return DataPortal.Fetch<T>();
122: }
123:
124: public T Fetch(object criteria)
125: {
126: return DataPortal.Fetch<T>(criteria);
127: }
128:
129: public async Task<T> FetchAsync(object criteria)
130: {
131: return await DataPortal.FetchAsync<T>(criteria);
132: }
133:
134: public async Task<T> FetchAsync()
135: {
136: return await DataPortal.FetchAsync<T>();
137: }
138:
139: public TC FetchChild<TC>()
140: {
141: return DataPortal.FetchChild<TC>();
142: }
143:
144: public TC FetchChild<TC>(params object[] parameters)
145: {
146: return DataPortal.FetchChild<TC>(parameters);
147: }
148:
149: public event EventHandler<DataPortalResult<T>> FetchCompleted;
150:
151: public ContextDictionary GlobalContext
152: {
153: get { return ApplicationContext.GlobalContext; }
154: }
155:
156: public T Update(T obj)
157: {
158: return DataPortal.Update<T>(obj);
159: }
160:
161: public async Task<T> UpdateAsync(T obj)
162: {
163: return await DataPortal.UpdateAsync<T>(obj);
164: }
165:
166: public event EventHandler<DataPortalResult<T>> UpdateCompleted;
167:
168: public void UpdateChild(object child)
169: {
170: DataPortal.UpdateChild(child);
171: }
172:
173: public void UpdateChild(object child, params object[] parameters)
174: {
175: DataPortal.UpdateChild(child, parameters);
176: }
177: }
This object factory implementation requires that our IoC container understand the concept of generics and the type of object that we pass in must implement CSLA's IMobileObject interface. The IBusinessBase, IBusinessListBase, IReadOnlyBase, IReadOnlyListBase and ICommandObject all implement this interface and will be usable for our custom object factory. If I always make sure to use the IObjectFactory interface to resolve out my DataPortal call I will be able to mock the entire thing out when writing unit tests.
To create our concrete classes we can follow this pattern:
1: public class TestBusinessBase : BusinessBase<TestBusinessBase>, ITestBusinessBase
2: {
3: public static readonly PropertyInfo<int> IdProperty = RegisterProperty<int>(c => c.Id);
4: public int Id
5: {
6: get { return GetProperty(IdProperty); }
7: private set { LoadProperty(IdProperty, value); }
8: }
9:
10: public static readonly PropertyInfo<string> NameProperty = RegisterProperty<string>(c => c.Name);
11: public string Name
12: {
13: get { return GetProperty(NameProperty); }
14: set { SetProperty(NameProperty, value); }
15: }
16:
17: public void SomeMethod(string someParameter)
18: {
19: throw new NotImplementedException();
20: }
21:
22: public static ITestBusinessBase CreateTechBusinessBase()
23: {
24: return IoC.Container.Resolve<IObjectFactory<ITestBusinessBase>>().Create();
25: }
26:
27: public async static Task<ITestBusinessBase> GetTestBusinessBaseByIdAsync(int id)
28: {
29: return await IoC.Container.Resolve<IObjectFactory<ITestBusinessBase>>().FetchAsync(id);
30: }
31: }
This is a sample of an implementation for our BusinessBase interface and all the stereotypes will generally look the same. There are a few of things to notice here:
- Our concrete class still inherits from CSLA's BusinessBase class as well as our ITestBusinessBase interface
- Using the static factory methods will tie anything using it to the concrete class TestBusinessBase to locate the factory method
- We always return our interface instead of the concrete class type
- We use the global IoC class to resolve by interface instead of calling the concrete object factory or the concrete TestBusinessBase. This allows us to return any implementation of the IObjectFactory and/or ITestBusinessBase we desire from the IoC container. This will be very useful when it comes to creating unit tests.
If you don't like the idea of using the static factory methods, any code that would call the factory method can call the object factory instead. The downside of this is if we have our factory methods abstract out knowledge of the criteria and parameters allowed to make it easy for the consumers of our classes. The person using our classes will instead need to know the particulars of the allowed parameters for the factory. For the above class we could call the following instead of calling the GetTestBusinessBaseByIdAsync factory method:
1: await IoC.Container.Resolve<IObjectFactory<ITestBusinessBase>>().FetchAsync(id);
To understand what we need to do next we have to understand a little about how CSLA's data portal works. When we ask it to create, fetch or save an object it creates an instance of that type (unless using the object factory pattern which is not covered here). The type to create is determined by the type set in the data portal generic. For example if I call the following:
1: DataPortal.Create<TestBusinessBase>()
The CSLA data portal will attempt to create an instance of the TestBusinessBase object and then call it's DataPortal_Create method. But that's not what our code ends up doing. What our code ultimately will call on the CSLA data portal is this:
1: DataPortal.Create<ITestBusinessBase>()
CSLA's data portal won't know what concrete class to create in this case. ITestBusinessBase is just an interface. So what do we do? This is where CSLA's IDataPortalActivator interface comes into play. This interface allows us to do two things:
- Tell CSLA what instance to create (CreateInstance method)
- Set any properties on that instance after it is created (InitializeInstance method)
There are a few items to note here. The CreateInstance method only receives one parameter, the type that is being requested. In normal CSLA operation this would be a concrete type but in our case we are going to be passed in the ITestBusinessBase interface. There is an implication to this as we do not have access to any other parameters sent along with the data portal call. This means we only have a few ways to create the required instance from the type information:
- A convention that given the type of the interface we are able to determine the concrete type we want to create
1: public object CreateInstance(Type requestedType)
2: {
3: if (requestedType == null)
4: {
5: throw new ArgumentNullException("requestedType");
6: }
7:
8: return requestedType.IsInterface ? CreateConcreteTypeByConvention(requestedType) : Activator.CreateInstance(requestedType);
9: }
- Pulling something out of global scope, such as an IoC container, to resolve out the concrete type we want to create
1: public object CreateInstance(Type requestedType)
2: {
3: if (requestedType == null)
4: {
5: throw new ArgumentNullException("requestedType");
6: }
7:
8: return requestedType.IsInterface ? IoC.Container.Resolve(requestedType) : Activator.CreateInstance(requestedType);
9: }
The InitializeInstance method allows us to set and initialize properties on the newly created instance but it only receives a reference to the instance of the class that was created in the CreateInstance method. Like the CreateInstance method, any information that we sent along with the initial data portal request is not directly available to us. For example, we cannot pass in the IoC container as a parameter to a factory method that uses the CSLA data portal and then use that parameter in the CreateInstance method to create an instance of the class we want without somehow storing it in a global context.
CSLA does provide a way to set up parameters in a global context that can be used in the CreateInstance and InitializeInstance methods. Implementing the IInterceptDataPortal interface will provide us with methods we can use for this purpose. This interface has Initialize and Complete methods that happen at the start of and end of our data portal call and allow us to set up and then clean up any global context needed. Be aware that the are some complications around the cleanup, particularly if an error occurs in the request. I won't be covering using the IInterceptDataPortal interface in this blog post. The best person to ask about using it is Jason Bock.
A sample IDataPortalActivator class may look like this:
1: public sealed class ObjectActivator : IDataPortalActivator
2: {
3: public object CreateInstance(Type requestedType)
4: {
5: if (requestedType == null)
6: {
7: throw new ArgumentNullException("requestedType");
8: }
9:
10: return requestedType.IsInterface ? IoC.Container.Resolve(requestedType) : Activator.CreateInstance(requestedType);
11: }
12:
13: public void InitializeInstance(object obj)
14: {
15: }
16: }
To tell CSLA to use our new activator instead of the default CSLA implementation call the following line in code:
1: Csla.ApplicationContext.DataPortalActivator = new ObjectActivator();
Notice this is also a service locator pattern. I want to reiterate this idea. Like ASP MVC, CSLA is built on top of service locators. This call is no different. In order to use frameworks like ASP MVC or CSLA you have to buy into the idea that the service locator pattern is sometimes desirable.
In this post we learned how to create and pass around CSLA objects as interfaces instead of concrete classes using the CSLA data portal. This will allow us much greater flexibility when it comes to unit testing and mocking. In my next post we will talk about how to similarly abstract away data access calls to assist in unit testing and mocking.
No comments:
Post a Comment