I mentioned yesterday on Twitter that I'm helping out the Oxite team with some refactoring and tweaking of their codebase. I got started last night and people have been asking about it a LOT, so I thought that today I'd show you initially what I'm up to.
First - the obvious: Oxite as it stands is not acceptable as a starting point for ASP.NET MVC, and they've pointed that out on there project site today. I'd like the pendulum to swing in the positive direction now, If I may.
Please know - this is an initial pass and some basic thoughts. It's very early and if you look at the source you'll see there are precisely ZERO tests. I'm going to change that in 34 minutes from now - I needed to get my head around the model quickly, and it involved diving head first into the existing code and extracting information - that's how I got to the point below.
Step 1: Understanding What The Client Wants
I've been talking to Duncan and Erik a bit of late (and others on their team) and have a good gauge of what they want (in terms of an overall guiding statement):
a highly-functional, scaleable CMS application that renders high quality and well-formatted semantic markup.
They're initially building this for the VisitMix site - so I'll use that as my "client" so I can try to gear what I make and have it be relevant.
Step 2: The Branch
After looking through the codebase and talking to Duncan and Erik, we thought it would be best to do a branch rather than implementing these changes directly in the trunk. So I'm putting this on my SVN server and If you want to view the source as I go, it's here:
http://www.wekeroad.com:8080/svn/Oxite/trunk
UPDATE: you need to login with "pub" as the user and "pub" as the password.
That's my own Subversion repo - when it comes time to move stuff over I'll work with the team to get in place.
Step 3: The Database
In looking over the database and running the application, it seems pretty clear what's needed here (but I'm sure I'll have to expand on this). For now my target is the existing functionality, as opposed to what's planned:
In essence (from a DB perspective), this is the everyman blog sample:
_beed7f32-75f3-47ed-8ec9-d28b38b5ea37.png)
Again I know it's basic - and no I'm not necessarily "starting from the database" - this is a weird situation where I have stuff right here in front of me, and I'm refactoring the existing system down - so in that sense I'm not really doing TDD here. But I promise - I will get to testing. Working on the DB first is helping me to understand the requirements and what the intent is. Please don't flame me :).
I've removed a good many things here. If you're familiar with the Oxite DB (as it was before I started this), it had some extra tables in it that I think we can let the framework handle. In summary, here are the main ones I've removed:
User, Role, and Role Relationships Tables- I'm going to lean on an IAuthentication/IAuthorization service for this, and initially use ASP.NET Auth - this will help existing blog apps (on the .NET platform) merge a bit more happily. Eventually I'll plug in Open ID.
Language, UserLanguage, StringResource Tables- these tables are basically for localization, but this is handled very nicely by the platform. I've removed them in favor of using Resources and System.Globalization.CultureInfo.
Subscriptions (and related tables) - Subscriptions are normally handled (these days) by RSS. I've removed for now simply to get the app on its feet (it wasn't enabled in the original rev). If we need an emailer, we can add later.
Messages (and related tables) - again this is removed until the scope is figured a bit. More of a forums angle - CMS apps don't usually provide this but again I'll add later.
Step 4: Project Structure
I know I'll get some sniggers on this :), but I've restructured the project to lean on a bit more of a DDD approach. I'm not going full-tilt DDD here on this first pass, but hopefully I can set some goodness up that we can extend later, should the need arise (and I'm sure it will).
The solution has five main project:
Here's how things look currently, I'll explain more in a second (the little dots next to the files are VisualSVN):
Step 4: The initial Domain Model
I'm laying these out in a hurry, and I'm running with scissors with respect to TDD. Let's just get this out of the way. I won't write a stitch of logic without a test behind it (well, I'll do my best) but right now I need to get this domain down, at least in terms of what's a service and what's an object. I know I'm violating things, but this is triage friends :).
You can see from the diagram above that I've got (basically) two Aggregate Roots: Post and User. Well in truth User isn't much of an Aggregate root right now - but I think it will get there soon enough :).
Post is the main object, and it's also abstract. The idea with the site is that a Post is a a bit of text a user can add to the site:
namespace Oxite.CMS {
public abstract class Post:EntityBase {
public Post() : base() { }
public Post(Guid id) : base(id) { }
public string LanguageCode { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public User Author { get; set; }
public DateTime DateCreated { get; set; }
}
}
Next up is Article, which inherits from Post:
namespace Oxite.CMS {
public class Article : Post, IAggregateRoot,IEntity {
public IList<Area> Areas { get; set; }
public LazyList<Comment> Comments { get; set; }
public DateTime DatePublished { get; set; }
public object IEntity.Key {
get { return this.Key; }
}
}
}
(Yes Colin, I've taken your advice on the IEntity stuff :)
I've also typed out Comment and Trackback in sort of the same way - they're not all that interesting so I'll save room here and keep moving.
Domain Services
Right now I have five separate domain services defined - but I think they'll get trimmed and tweaked. This is just the start:
Step 5: Feeds, etc
I'm going to lean on the ASP.NET MVC bits to handle this. I'm not totally sure how that will work, but I know Phil has some ideas :). There's some blog expertise in our group.
Love to hear your comments. Yes I know there are no tests - getting to that. This is triage.
Positive and affirmative action.
webapp. Can you use Tortoise out of curiosoty?
:)
Thoughts?
You're right - they are sort of the same, but a sense of structure is implied. Also I think tagging is meant to be a public functionality
Way to go Rob and good luck with the process!
I admit that I haven't looked into what LazyList actually does, so maybe it addresses these issues.
I have a pretty long history of working with CMS's and I would love to help out if you think there is an opportunity.
Good point RE caching.
disqus. If I were doing this from scratch id like to see comments
implemented as a separate resource with at least json, atom, and rss
representations. this way you can ajax them into the page or point an xsl
transformer at the atom version to render them inline if you must.
I guess the real thrust of my point is to be careful about the degree to
which related but separate resources are combined during rendering because
this can have disproportionally adverse effects on the rest of the site.
Also, for the content bucketing - I would think about introducing an
IContentMetadata service that can plug into whatever semantic/contextual
system you need and provide a way to drive content aggregation by using that
data. I haven't seen this approach taken yet but I think its closer to what
most people really want. By default you could simply ask for the metadata
but things get super interesting when you hook it into something like
daylife, opencalais or fast esp.
Area-a, Area-b and Area-51 might all be related and each tagged as .NET but Area-a is about C# bleeding edge, Area-b is about asp.net webforms and Area-51 is the cool area devoted to asp.net mvc! Checkout http://stackoverflow.com/tags for a good implementation of tags.
Can you manage to fix this?
Maybe setting up the repository access level to "read only" for anonymous users, and based on individual request you can assign higher permission set to users who requested directly to be
a part of the development.
Rant over - good luck!
I hate it when you try to make things easy for people and you find you need to do stuff like that to make it work.
It's not that important, but I'd love to see a drop of the code sometime soon anyway.
Your SVN server requested a username and a password. Can you help?
Bart
You talk about all of these projects you "work" on but it hardly sounds like work (ASP.NET MVC, Storefront, Subsonic, Oxite, etc.). I'm assuming, of course, that these projects are part of your duties at MSFT. Must be nice to work on really cool apps that the community eats up.
I need to finally start one of my OS ideas and see how it goes...
I'm looking forward to seeing how this develops. I'm trying to connect to the svn repo but I'm getting prompted for a username/password. Any suggestions?
Thanks
setup on Apache. I'll drop it now - but I need to turn it back on when
comitting and Hammett was on it last night.
What's your take on this for Oxite?
order = GetOrder(orderID)
order.Items = GetOrderItems(orderID, iitemSortName, itemSortOrder, itemFilterBy, itemFilter, itemStartPage, itemPageSize)
order.Attachments = GetOrderAttachments(orderID, attachSortName, attachSortOrder, attachFilterBy, attachFilter, attachStartPage, attachPageSize)
then refactor your service layer accordingly
They can be useful for equality, but identity map solves this by making object with same keys reference equal to each other. You obviously need key in some parts of UI, but this seems like something a KeyService or Repository can provide. Same goes for NewKey(). This basically removes the need for EntityBase and IEntity (or turns IEntity into a marker interface).
Also you are explicitly declaring a property as LazyList -- would not it be better to have an IList which is a LazyList when required?
I believe Akismet implementation shouldn't be in Create action in CommentController. I would decouple it to have more flexible code.
Here is my thoughts:
http://tunatoksoz.com/post/Decoupled-design-with-events.aspx
Thanks for your attention
is interesting but to me it's adding ceremony to what amounts to a boolean
rule check... but I'm listening :).
My reasoning is that spam service is one of the many possibilities that can be done with the comment. Other possibilities, for example, are sending a notification to the author of the post that there is a new comment etc. Thinking on those, I see that adding those new actions to the controller would be ugly and an indicator of SRP, too. If you use event handlers, new handlers can easily be attached, and you'll have cleaner code.
preferable to direct method invocation. That said - the BlogService is my
business logic and it seems fairly straightforward with what's in front of
me. So walk me through this - where do I wire the event? And how is the
process clearer than putting the spam check / email author calls into the
business method? I'm very interested...
You can wire up the events using containers, this is a piece of cake.
of what you mean. I'm trying to work from simple --> complex and right now I
don't see the need to craft an event then put in the wiring logic - all to
replace a single call. I do believe you have a point, I'm just having a hard
time getting there so I need your help :)
Thanks for you attention, Rob. I really appreciate it.
least. I do get how events will loosen things up - I just want to see more
of what you're talking about. I trust your judgement here :) I'm just way
too visual.
Please...?
The thing that me and you differentiate at is (probably) that I am thinking a little further, and trying to provide extension mechanism at the beginning, while you are doing things as you come across, ie iteratively. Maybe you would do this my way when needs require this.
On the other hand, I still think that Spam Check is better to be a seperate concern and this seperation brings more flexibility.
Anyway, I started to repeat myself, I probably cannot find the right technical term for this issue.
I liked this discussion as it made me think again and again. Thanks, Rob!
I really appreciate your appropriate and level-headed reaction to this entire scenario. It would be interesting if you could turn this whole thing into a transparent learning experience as you are doing with the MVC Storefront. I can already see that you are going to be transparent in your analysis and redesign, but I think In this case I'm also interested in the learning experience the original Oxite team will be going through. It could be very educational for everyone if handled in the right way.
since oxite is a database related application, and why can't I figure out any transaction comtrol code in entire code?
Is for some reason or I missed something?
1) Give up, work on a different project. Oxite's not going to fly
2) wait until Rob's fork is merged and see if he did it first
3) Go ahead.
?
1. Instead of using System.Configuration.ConfigurationManager.AppSettings as the store for wordpress api key, wouldn't it be better to have it injected, to increase easier testability? (The same thing goes with AccountController:OpenId)
I _believe_ it is better to have something like IAuthenticationProvider to hide OpenId details, it's place, imho, is not the controller.
And also passing "localhost" may not be a good thing, but it doesn't matter much.
2.I am not sure if this returns null ever (in LocalizationService)
List
where p.LanguageCode == languageCode
select p).ToList();
By convention, it should return a list with no element.
3. I believe(no strong thoughts on that)
public LazyList
I would personally return IList
i am learning those code,but sometimes it confuse me,
i bid your blog can help me a lot
great thanks
http://www.wekeroad.com:8080/svn/Oxite/trunk/Oxite.Web/Controllers/AccountController.cs
This uses RPXNow and is a really nice way of doing Open ID.
are worked here :). do you think setting these things in Bootstrapper would
be the way to go?
just wondering what you all might consider doing.
would do - I'd probably fight the requirement :)
To me there are going to be other business rules in place that would suggest the identity of an entity instead of some randomly created GUID.
I mean employees/users for example. They can have the same name, they can have the same address, heck with identity theft they can have the same everything. That is why companies created EmployeeID or why you have Usernames. A user/employee will never have the same username or employeeID in your system (or else they couldn't log in) and i would guess that a GUID would never be your choice to represent that. So why even have it.
Again not a huge deal cause you can overwrite that functionality. But why have it there. I mainly ask because I simply could be missing something.
I wrote a test entities_can_test_for_equality().
I tried testing with both a Guid and int. I created to TestEntity objects with the same Guid and/or int.
the I tried an Assert.AreEqual(testEntityA, testEntityB); it failed. Id I test the two keys directly it works. I tried debugging.
"if (base1.Key != base2.Key)" from the EntityBase is true even though the keys are the same, shouldn't it be false.
Can't figure it out. So I thought I would ask the people who are here, as you are all probably smarter than I.
from
"if (base1.Key != base2.Key)"
to
if (base1.Key.GetHashCode() != base2.Key.GetHashCode())
not sure why but this works.
Am I right?
I just want to learn and know how to use and define transaction boundary in this refator architecture design.
From looking at the current snapshot it seems that you've ended up with an Anemic Domain Model (not criticizing, just curious as I learn DDD concepts). Is that by choice or is Oxite simply devoid of "validation, calculations and business logic" that you might find in a "business application"? In Nilsson's book his DDD experiences have resulted in him putting most of this business logic in his service layers so that he can provide different implementations of business logic using the same domain models. But the majority of articles I've read say to start with your logic in your domain models and move it to services only when it no longer fits.
What say you?
this from the ground up, per se. There's not much logic present since the
app is quite simple now - but as it grows I'm sure you'll see the services
grow too.
Positive and affirmative action.
Way to go Rob and good luck with the process!
webapp. Can you use Tortoise out of curiosoty?
:)
</rant>
Thoughts?
You're right - they are sort of the same, but a sense of structure is implied. Also I think tagging is meant to be a public functionality
Area-a, Area-b and Area-51 might all be related and each tagged as .NET but Area-a is about C# bleeding edge, Area-b is about asp.net webforms and Area-51 is the cool area devoted to asp.net mvc! Checkout http://stackoverflow.com/tags for a good implementation of tags.
What's your take on this for Oxite?
I admit that I haven't looked into what LazyList actually does, so maybe it addresses these issues.
I have a pretty long history of working with CMS's and I would love to help out if you think there is an opportunity.
Good point RE caching.
disqus. If I were doing this from scratch id like to see comments
implemented as a separate resource with at least json, atom, and rss
representations. this way you can ajax them into the page or point an xsl
transformer at the atom version to render them inline if you must.
I guess the real thrust of my point is to be careful about the degree to
which related but separate resources are combined during rendering because
this can have disproportionally adverse effects on the rest of the site.
Also, for the content bucketing - I would think about introducing an
IContentMetadata service that can plug into whatever semantic/contextual
system you need and provide a way to drive content aggregation by using that
data. I haven't seen this approach taken yet but I think its closer to what
most people really want. By default you could simply ask for the metadata
but things get super interesting when you hook it into something like
daylife, opencalais or fast esp.
I hate it when you try to make things easy for people and you find you need to do stuff like that to make it work.
It's not that important, but I'd love to see a drop of the code sometime soon anyway.
Can you manage to fix this?
Maybe setting up the repository access level to "read only" for anonymous users, and based on individual request you can assign higher permission set to users who requested directly to be
a part of the development.
Rant over - good luck!
Your SVN server requested a username and a password. Can you help?
Bart
You talk about all of these projects you "work" on but it hardly sounds like work (ASP.NET MVC, Storefront, Subsonic, Oxite, etc.). I'm assuming, of course, that these projects are part of your duties at MSFT. Must be nice to work on really cool apps that the community eats up.
I need to finally start one of my OS ideas and see how it goes...
I'm looking forward to seeing how this develops. I'm trying to connect to the svn repo but I'm getting prompted for a username/password. Any suggestions?
Thanks
setup on Apache. I'll drop it now - but I need to turn it back on when
comitting and Hammett was on it last night.
order = GetOrder(orderID)
order.Items = GetOrderItems(orderID, iitemSortName, itemSortOrder, itemFilterBy, itemFilter, itemStartPage, itemPageSize)
order.Attachments = GetOrderAttachments(orderID, attachSortName, attachSortOrder, attachFilterBy, attachFilter, attachStartPage, attachPageSize)
then refactor your service layer accordingly
They can be useful for equality, but identity map solves this by making object with same keys reference equal to each other. You obviously need key in some parts of UI, but this seems like something a KeyService or Repository can provide. Same goes for NewKey(). This basically removes the need for EntityBase and IEntity (or turns IEntity into a marker interface).
Also you are explicitly declaring a property as LazyList -- would not it be better to have an IList which is a LazyList when required?
I believe Akismet implementation shouldn't be in Create action in CommentController. I would decouple it to have more flexible code.
Here is my thoughts:
http://tunatoksoz.com/post/Decoupled-design-wit...
Thanks for your attention
is interesting but to me it's adding ceremony to what amounts to a boolean
rule check... but I'm listening :).
My reasoning is that spam service is one of the many possibilities that can be done with the comment. Other possibilities, for example, are sending a notification to the author of the post that there is a new comment etc. Thinking on those, I see that adding those new actions to the controller would be ugly and an indicator of SRP, too. If you use event handlers, new handlers can easily be attached, and you'll have cleaner code.
preferable to direct method invocation. That said - the BlogService is my
business logic and it seems fairly straightforward with what's in front of
me. So walk me through this - where do I wire the event? And how is the
process clearer than putting the spam check / email author calls into the
business method? I'm very interested...
You can wire up the events using containers, this is a piece of cake.
of what you mean. I'm trying to work from simple --> complex and right now I
don't see the need to craft an event then put in the wiring logic - all to
replace a single call. I do believe you have a point, I'm just having a hard
time getting there so I need your help :)
Thanks for you attention, Rob. I really appreciate it.
least. I do get how events will loosen things up - I just want to see more
of what you're talking about. I trust your judgement here :) I'm just way
too visual.
Please...?
The thing that me and you differentiate at is (probably) that I am thinking a little further, and trying to provide extension mechanism at the beginning, while you are doing things as you come across, ie iteratively. Maybe you would do this my way when needs require this.
On the other hand, I still think that Spam Check is better to be a seperate concern and this seperation brings more flexibility.
Anyway, I started to repeat myself, I probably cannot find the right technical term for this issue.
I liked this discussion as it made me think again and again. Thanks, Rob!
1. Instead of using System.Configuration.ConfigurationManager.AppSettings as the store for wordpress api key, wouldn't it be better to have it injected, to increase easier testability? (The same thing goes with AccountController:OpenId)
I _believe_ it is better to have something like IAuthenticationProvider to hide OpenId details, it's place, imho, is not the controller.
And also passing "localhost" may not be a good thing, but it doesn't matter much.
2.I am not sure if this returns null ever (in LocalizationService)
List<Phrase> result= (from p in _phraseRepo.GetPhrases()
where p.LanguageCode == languageCode
select p).ToList();
By convention, it should return a list with no element.
3. I believe(no strong thoughts on that)
public LazyList<Tag> Tags { get; set; }
I would personally return IList<Tag> for the sake of abstraction.(even though it hides some detail)
are worked here :). do you think setting these things in Bootstrapper would
be the way to go?
I really appreciate your appropriate and level-headed reaction to this entire scenario. It would be interesting if you could turn this whole thing into a transparent learning experience as you are doing with the MVC Storefront. I can already see that you are going to be transparent in your analysis and redesign, but I think In this case I'm also interested in the learning experience the original Oxite team will be going through. It could be very educational for everyone if handled in the right way.
since oxite is a database related application, and why can't I figure out any transaction comtrol code in entire code?
Is for some reason or I missed something?
1) Give up, work on a different project. Oxite's not going to fly
2) wait until Rob's fork is merged and see if he did it first
3) Go ahead.
?
http://www.wekeroad.com:8080/svn/Oxite/trunk/Ox...
This uses RPXNow and is a really nice way of doing Open ID.
i am learning those code,but sometimes it confuse me,
i bid your blog can help me a lot
great thanks
just wondering what you all might consider doing.
would do - I'd probably fight the requirement :)
To me there are going to be other business rules in place that would suggest the identity of an entity instead of some randomly created GUID.
I mean employees/users for example. They can have the same name, they can have the same address, heck with identity theft they can have the same everything. That is why companies created EmployeeID or why you have Usernames. A user/employee will never have the same username or employeeID in your system (or else they couldn't log in) and i would guess that a GUID would never be your choice to represent that. So why even have it.
Again not a huge deal cause you can overwrite that functionality. But why have it there. I mainly ask because I simply could be missing something.
I wrote a test entities_can_test_for_equality().
I tried testing with both a Guid and int. I created to TestEntity objects with the same Guid and/or int.
the I tried an Assert.AreEqual(testEntityA, testEntityB); it failed. Id I test the two keys directly it works. I tried debugging.
"if (base1.Key != base2.Key)" from the EntityBase is true even though the keys are the same, shouldn't it be false.
Can't figure it out. So I thought I would ask the people who are here, as you are all probably smarter than I.
from
"if (base1.Key != base2.Key)"
to
if (base1.Key.GetHashCode() != base2.Key.GetHashCode())
not sure why but this works.
Am I right?
I just want to learn and know how to use and define transaction boundary in this refator architecture design.
From looking at the current snapshot it seems that you've ended up with an Anemic Domain Model (not criticizing, just curious as I learn DDD concepts). Is that by choice or is Oxite simply devoid of "validation, calculations and business logic" that you might find in a "business application"? In Nilsson's book his DDD experiences have resulted in him putting most of this business logic in his service layers so that he can provide different implementations of business logic using the same domain models. But the majority of articles I've read say to start with your logic in your domain models and move it to services only when it no longer fits.
What say you?
this from the ground up, per se. There's not much logic present since the
app is quite simple now - but as it grows I'm sure you'll see the services
grow too.