Plateforme Level Extreme
Abonnement
Profil corporatif
Produits & Services
Support
Légal
English
New exception?
Message
Information générale
Forum:
ASP.NET
Catégorie:
Code, syntaxe and commandes
Titre:
Versions des environnements
Environment:
C# 4.0
OS:
Windows 7
Network:
Windows 2003 Server
Database:
MS SQL Server
Divers
Thread ID:
01554234
Message ID:
01554341
Vues:
84
This message has been marked as the solution to the initial question of the thread.
It's a business object so this has nothing really to do with MVC or any other platform. It can work in any .NET project.

You create a business object like this:
public class busEntry : EfCodeFirstBusinessBase< Entry,ClassifiedsContext  >
{

}
You provide two generic type parameters: The base EF Entity type (Entry in this case) and the DbContext (or EfCodeFirstContext optionally). Just doing this gives you all the base functionality of the business object which includes full CRUD operations (Load,NewEntity,Save,Delete) plus validation, error handling and a number of event hooks that allow for defaults and fixups of values before/after updates.

To use then is easy:
var entryBus = new busEntry();

var entry = entryBus.NewEntity();
entry.Title = "Title";
entry.Entered = DateTime.Now;

if (!entryBus.Save())
   ErrorDisplay.ShowError( entryBus.ErrorMessage );

int id = entryBus.Entity.Id

// now load the saved entity from disk
var entry2 = entryBus.Load(id);

model.Title = entry2.Title;
model.Entered = entry2.Entered;

entry2.Updated = DateTime.Now;

if (entryBus.Validate())   //  can also happen automatically
   entryBus.Save();  
This handles all the business of creating a new entity, adding it to the context and managing lifetime of the object. It handles all basic CRUD operations so you generall don't access the DbContext directly outside of the business object itself.

The idea of the business object is to map a logical set of operations. So even though you map a single entity, that doesn't mean the business object only deals with that single entity. This business object for example has related images and the images are managed through this business object and not its own.

To make the business object useful you add methods that act on the entity assigned to it. Most of these methods internally just use the DbContext.

Query Methods just return IQueryable results. Here's are a couple of examples of a simple one and a more complex one that deals with query parameters:
public IQueryable<Entry> GetEntryList()
{
    return Context.Entries
                .Take(50)
                .OrderByDescending(e => e.Entered);
}

public IQueryable<EntryListItem> GetEntryList(EntryListQueryParameters parms = null, AppTypes appType = AppTypes.Empty)
{
    if (parms == null)
        parms = new EntryListQueryParameters();

    if (appType == AppTypes.Empty)
        appType = App.Configuration.AppType;

    if (parms.SearchPhrase == null)
        parms.SearchPhrase = string.Empty;

    string[] searchPhrases = null;
    if (parms.SearchPhrase.Contains(','))
        searchPhrases = parms.SearchPhrase.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
    else
        searchPhrases = new string[1] { parms.SearchPhrase };


    var entries = Context.Entries
                            .Where(ent => ent.AppType == (int)appType &&
                                ent.IsActive &&
                                ent.Entered >= parms.StartDate &&
                                ent.Entered <= parms.EndDate)
                            .Select( ent=> new EntryListItem() 
                            {   Id = ent.Id,
                                DisplayId = ent.DisplayId,                                        
                                Title = ent.Title, 
                                Description = ent.Description,
                                CategoryId = ent.CategoryId, 
                                Entered = ent.Entered,
                                ImageCount = ent.Images.Count(),
                                Location = ent.Location,
                                PriceString = ent.PriceString,
                                Category = ent.Category,
                                Keywords = ent.Keywords
                            });

                
    if (!string.IsNullOrEmpty(parms.Location))
        entries =  entries.Where(ent => ent.Location.StartsWith(parms.Location));

    if (parms.Category > 0)
    {
        // Special Category
        if (parms.Category >= 1000)
        {
            if (parms.Category == (int)SpecialCategories.AllCategoriesButRealEstate)                    
                entries = entries.Where(ent => !ent.Category.Name.StartsWith("Real Estate"));                    
        }
        else
            entries = entries.Where(ent => ent.CategoryId == parms.Category);
    }

    if (!string.IsNullOrEmpty(parms.Keyword))
        entries = entries.Where(ent=> ent.Keywords.Contains(parms.Keyword));            

    switch (parms.SortOrder)
    {
        case(EntryListSortOrder.Category):
            entries = entries
                            .OrderBy(ent => ent.Category.Name)
                            .ThenByDescending(ent => ent.Entered);
            break;
        case(EntryListSortOrder.Date):
            entries = entries
                            .OrderByDescending(ent => ent.Entered);                                  
            break;
    }

    return entries;
}
Here's a couple of operational method that updates some data (in this case from an AJAX operation on the site typically)
        public bool ReportAsAbuse(int entryId, string reason, int userId, bool undoAbuse = false)
        {
            if (!undoAbuse)
            {
                // no bus object for abuses so we manually use context
                var abuse = new ReportedAbuse();
                abuse.EntryId = entryId;
                abuse.Reason = reason;
                abuse.UserId = userId;
                Context.ReportedAbuses.Add(abuse);
                
                this.Load(entryId);
                this.Entity.IsFlagged = true;

                return this.Save();  // saves both abuse and entry
            }
            
            
            return UndoAbuse(entryId);            
        }

        public bool IsDuplicateNewEntry(Entry entry)
        {
            // if we're not adding a new record - not a dupe             
            if (entry.Id != 0)
                return false; 

            var date = DateTime.Now.AddDays( App.Configuration.EntryExpirationDays * -1);
            return Context.Entries.Any(ent =>
                    ent.DisplayId == entry.DisplayId ||
                    (ent.UserId == entry.UserId &&
                     ent.Title == entry.Title && 
                     ent.Entered > date));
        }
Then you can also implement things like various event hooks fired before and after saving on loading etc as well as validation of the business object:
protected override bool OnBeforeSave(Entry entity)
{           
    if (entity != null)
    {
        entity.Updated = DateTime.Now;
        entity.AppType = (int)App.Configuration.AppType;
                
        // silently strip out 'highlight' characters
        entity.Title = CleanupTitle(entity.Title);

        entity.PriceString = this.FormatPriceString(entity.PriceString);                
    }

    return true;
}

/// <summary>
/// Overridden to handle base validation
/// </summary>
/// <param name="entity"></param>
protected override void OnValidate(Entry entity)
{
    base.OnValidate(entity);

    if (entity.CategoryId == null || entity.CategoryId == 0)
        ValidationErrors.Add("Please choose a category", "CategoryId");
    if (string.IsNullOrEmpty(entity.Title))
        ValidationErrors.Add("Please enter a title for your listing", "Title");
    if (string.IsNullOrEmpty(entity.Description))
        ValidationErrors.Add("Please enter a description for your listing", "Description");
    if (entity.UserId == null || entity.UserId == 0)
        ValidationErrors.Add("You're not logged in.");             
    if (string.IsNullOrEmpty(entity.ContactEmail) )
        ValidationErrors.Add("Please provide a contact email for your listing", "ContactEmail");
}

/// <summary>
/// overridden to delete child images
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
protected override bool OnBeforeDelete(Entry entity)
{
    if (entity.Images != null)
        entity.Images.ToList().ForEach(img => { Context.Images.Remove(img); });
            
    return true;
}
        
protected override bool OnAfterDelete(Entry entity)
{            
    this.ExecuteNonQuery("delete from ReportedAbuses where entryId=@id",CreateParameter("@id",entity.Id));
    this.ExecuteNonQuery("delete from Images where entryId=@id",CreateParameter("@id",entity.Id));
            
    return true;
}
Note also that this includes the optional ability to execute raw SQL against the provider easily in a variety of ways. Optoinally if you use EfCodeFirstContext there's also an SqlNative property that provides full low level data access with classic ADO.NET support if that's ever needed (and I find that in every app there are always 1 or 2 requests that require this whether for performance or querying that simply doesn't work with LINQ or StoreProcedure call access).

This class is just a fairly lightweight wrapper around EFCodeFirst, but it provides a ton of wrapped up functionality that you'd otherwise have to add to each repository if you use the repository pattern. I personally prefer this approach and it's a culmination of a few years of working with context based ORMs (this started as a Linq 2 SQL project originally). It's worked great for me...

As to one DbContext per business object: Internally the business object needs access to the context so it needs to have an instance. The idea is that the context belongs to the operations it acts on and having one context per bus object allows multiple context in scope at the same time and caching entities. If you need to share a context you can pass an existing context in the constructor, but frankly I don't think that's a good idea except for explicit cases where business objects need to be nested.

Hope this helps,

+++ Rick ---

>Rick,
>
>I looked at the class code and liked what I saw.
>
>I have a few questions:
>
>1. Can I use it as is (just get rid of two WW specific references - didn't see immediately how they are used)
>
>2. Can you please show a quick sample of how you use this class in your MVC application?
>
>
>Also, what is the difference of creating one instance of DBContext per application vs. 1 DBContext per entity/business object?
>
>
>
>>Hi Naomi,
>>
>>Don't take this the wrong way, but there are few things problematic with this code.
>>
>>First it's not a repository pattern at all since the class is mixing the repository with the actual data access layer (DbContext). The whole point of a repository is that it's separate and potentially replaceable which is impossible with the code you have here because the DbContext is directly built into the class.
>>
>>The general idea for a repository - or even a more classic business object type approach - is to have the data access layer a property on the repository/business object rather than a single object that does all this in one place. This allows for better abstraction and isolation and possible replacement of the data access layer.
>>
>>Generally for Entity Framework code you create your model and your DbContext class, and then a separate repository class works with that model either in static methods which create the context and do atomic operations on the context/model, or alternately (which is more familiar to Fox developers) you have a 'business object' class that acts like a repository that's an instance and you fire methods on.
>>
>>Also your code doen't need all those IQueryable properties. DbSet can be directly accessed to get an IQueryable at any time. Anywhere IQueryable is used you can pass a DbSet to. Most likely though anytime you use the DbSet you will automatically apply a .Where() or OrderBy() or .FirstOrDefault() etc. that will automatically cast the result to the appropriate type. Get rid of those properties - they serve absolutely no purpose...
>>
>>
>>Personally I prefer the old school business object type class - which offers more flexibility and reuse as you can easily implement base CRUD operations. If you're interested take a look at the Westwind.BusinessFramework EfCodeFirstBusinessBase< TEntity,TContext > that provide a ton of functionality without any code at all. The class code is available - you can probably get some ideas from that:
>>
>>http://goo.gl/PxtxP
>>
>>Basically it's one class per logical business entity (which doesn't necessarily map a single EF entity)... I've been using this framework in various backend inplementations from LINQ to SQL, original Entity Framework and now with EFCodeFirst for about 5 years and it's been working great. It's pretty small and fairly self-contained (two main classes plus a few support helpers for raw SQL access if necessary and Validation).
>>
>>There are lots of ways to skin this cat of course but for my once FoxPro centric brain the above methodology to data access has served me well for well over 20 years :-)
>>
>>+++ Rick ---
>>
>>>This is MVC application I am working on. I have the Repository class with this code:
>>>
>>>
>>>using System.Data.Entity;
>>>using System.Linq;
>>>using CardNumbers.Objects;
>>>
>>>namespace CardNumbers.Data
>>>{
>>>    public class Repository : DbContext, IRepository
>>>    {
>>>        public DbSet<Client> Clients { get; set; }
>>>        public DbSet<ClientOrder> ClientOrders { get; set; }
>>>        public DbSet<Reorder> Reorders { get; set; }
>>>        public DbSet<Operator> Operators { get; set; }
>>>
>>>        IQueryable<Client> IRepository.Clients
>>>        {
>>>            get { return Clients; }
>>>        }
>>>
>>>        IQueryable<ClientOrder> IRepository.ClientOrders
>>>        {
>>>            get { return ClientOrders; }
>>>        }
>>>
>>>        IQueryable<Operator> IRepository.Operators
>>>        {
>>>            get { return Operators; }
>>>        }
>>>
>>>        IQueryable<Reorder> IRepository.Reorders
>>>        {
>>>            get { return Reorders; }
>>>        }
>>>
>>>        public void Commit()
>>>        {
>>>            this.SaveChanges();
>>>        }
>>>
>>>        public void AddClient(Client client, bool autoCommit = true)
>>>        {
>>>            Clients.Add(client)   ;
>>>            if (autoCommit) Commit();
>>>        }
>>>
>>>        public void DeleteClient(Client client, bool autoCommit = true)
>>>        {
>>>            Clients.Remove(client);
>>>            if (autoCommit) Commit();
>>>        }
>>>
>>>        public void DeleteClient(int clientId, bool autoCommit = true)
>>>        {
>>>            var ReordersQuery = from reord in this.Reorders
>>>                                where reord.ClientId == clientId
>>>                                select reord;
>>>
>>>            if (ReordersQuery.Any())
>>>               // throw "Client " + clientId.ToString() + " can not be deleted because it has related rows in the ReOrders table!";
>>>
>>>        }
>>>
>>>
>>>        public void AddOperator(Operator oOperator, bool autoCommit = true)
>>>        {
>>>            Operators.Add(oOperator);
>>>            if (autoCommit) Commit();
>>>        }
>>>
>>>        public void DeleteOperator(Operator oOperator, bool autoCommit = true)
>>>        {
>>>            Operators.Remove(oOperator);
>>>            if (autoCommit) Commit();
>>>        }
>>>
>>>        public void AddOrder(ClientOrder clientorder, bool autoCommit = true)
>>>        {
>>>            ClientOrders.Add(clientorder);
>>>            if (autoCommit) Commit();
>>>        }
>>>    }
>>>}
>>>
>>>
>>>Since I am going to call DeleteClient method, I am thinking I may need less generic variation and manually test dependencies myself. So, I started to write a version that will delete based on id.
>>>
>>>Perhaps I don't need this method?
>>>
>>>I started to write it based on this logic I found in MSDN after I googled on 'LINQ delete' http://msdn.microsoft.com/en-us/library/bb386925.aspx
>>>
>>>
>>>
>>>
>>>>Let's backup a bit.
>>>>
>>>>Is this a web app or desktop? Where is the context defined?
>>>>
>>>>>Hi everybody,
>>>>>
>>>>>I am writing a method to delete a cient in my Repository class. Here is what I've started from:
>>>>>
>>>>>
>>>>>public void DeleteClient(int clientId, bool autoCommit = true)
>>>>>        {
>>>>>            var ReordersQuery = from reord in this.Reorders
>>>>>                                where reord.ClientId == clientId
>>>>>                                select reord;
>>>>>
>>>>>            if (ReordersQuery.Any())
>>>>>                //throw "Client " + clientId.ToString() + " can not be deleted because it has related rows in the ReOrders table!";
>>>>>
>>>>>        }
>>>>>
>>>>>I read help on throw as I need to throw a new exception. My question is - what kind of exception it should be?
>>>>>
>>>>>Thanks in advance.
+++ Rick ---

West Wind Technologies
Maui, Hawaii

west-wind.com/
West Wind Message Board
Rick's Web Log
Markdown Monster
---
Making waves on the Web

Where do you want to surf today?
Précédent
Répondre
Fil
Voir

Click here to load this message in the networking platform