Many areas of a WCF and EF data access web service could be improved or enhanced to make the application more robust and valuable. In this article, you will see a sample application showing how and what can be done to achieve this.
Introduction
SOAP based web services are a reliable, scalable, and secure form of wrapping the data access layer (DAL) and business logic layer (BLL) in a multi-tiered business data applications, particularly in an enterprise environment. Negative arguments for this data access architecture are mostly focused on performance and complexity. Those disadvantages have decreased with the advances of WCF and Entity Framework. On the other hand, improvements and enhancements of structures and code can further increase the service performance and maintainability, and reduce the complexity. This article will describe how to set up the sample application first and then the details on the below highlighted topics:
Setting Up Sample Application
The StoreDB
sample database can be created in the SQL Server 2012 Express or LocalDB, and then populate the tables by executing the included script file, StoreDB.sql, using the Visual Studio or the SQL Server Management Studio (SSMS). You can download the SQL Server Express with LocalDB and the SSMS here.
The WCF web service is set to the basicHttpBinding
mode and hosted on an ASP.NET website. An EF data model with the database-first approach is set in a class library project. You can directly compile the downloaded source easily with the Visual Studio 2010 with the .NET Framework 4.0 or Visual Studio 2012/2013 with the .NET Framework 4.5. All referenced assemblies other than the .NET Framework are included in the Referenced.Lib folder under the sample application source root. Projects in the Visual Studio solution are shown in the below screenshot:
You need to change the data source
value "(localdb)\v11.0" for the connectionString
in the SM.Store.Service.web.config file to your SQL Server instance if you do not use the SQL Server 2012 LocalDB.
<add name="StoreDataEntities"
connectionString="metadata=res://*/StoreDataModel.csdl|res://*/StoreDataModel.ssdl|
res://*/StoreDataModel.msl;provider=System.Data.SqlClient;
provider connection string="data source=(localdb)\v11.0;
initial catalog=StoreDB;integrated security=True;multipleactiveresultsets=True;
App=EntityFramework"" providerName="System.Data.EntityClient" />
Moving Entity Classes to a Standalone Assembly
New models created using the EF designer generates a derived DbContext
and POCO type entity classes that are in the same DAL project. For better structures and component usability, we can move the model template file and all entity class files to a separate project, or assembly when compiled. To automatically refresh entity class contents by using Run Custom Tool context-menu command after any update with the model, we need to manually edit some code lines in both data context and data model template files to make the synchronization work. Here are examples as in the sample application.
In the StoreDataModel.Context.tt file from the SM.Store.Service.DAL
project:
In the StoreDataModel.tt file from the SM.Store.Service.Entities
project.
In an optimized implementation, the Unit of Work (UOW) object should initiate the data context object and be passed to repositories to coordinate the work of multiple repositories that share the single database context class. Since the UOW is on the top of the database context, the database connection string can be passed from the caller to the UOW class instead of directly injecting to the data context class.
public class StoreDataModelUnitOfWork : IUnitOfWork
{
private StoreDataEntities context;
public StoreDataModelUnitOfWork(string connectionString)
{
this.context = new StoreDataEntities(connectionString);
}
}
To make this connection string injection complete, we need to edit the context template StoreDataModel.Context.tt file for the data context class constructor. Each time when saving the file or running the custom tool for the template, the connection string injection code will be kept there.
The optimized structures of repositories should include a generic base repository class for the basic CRUD operations and other individual repositories inheriting the generic repository for the particular entity types. If operations for an entity are no more than those already in the generic repository, the derived repository for this entity may not be needed. The generic repository can directly be called from the BLL (see the example in the BLL section below).
In the generic repository class:
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
{
public IUnitOfWork UnitOfWork { get; set; }
public GenericRepository(IUnitOfWork unitOfWork)
{
this.UnitOfWork = unitOfWork;
}
}
In the individually typed repository class for the Product
entity:
public class ProductRepository :
GenericRepository<Entities.Product>, IProductRepository
{
public ProductRepository(IUnitOfWork unitOfWork)
: base(unitOfWork)
{
}
}
Calling Multiple Repositories from Business Logic Layer (BLL)
Although the primary purpose for the BLL is to enforce business rules, we can also use this layer to manage operations using multiple repositories, for example, saving data to multiple entities in cascading and transaction styles. This will keep the DAL in a clear single entity related structure. In a BLL class, the instances of multiple repositories are passed into the constructor for working with the dependency injection pattern (described in the below section).
public class CategoryBS : ICategoryBS
{
private IGenericRepository<Entities.Category> _categoryRepository;
private IProductRepository _productRepository;
public CategoryBS(IGenericRepository<Entities.Category>
cateoryRepository, IProductRepository productRepository)
{
this._categoryRepository = cateoryRepository;
this._productRepository = productRepository;
}
}
Configuring Dependency Injection (DI) with Unity
Using Microsoft Unity libraries, we can easily implement the dependency injection pattern for decoupling components of the service yet maintaining maximum co-adherence in these components. One of the key tasks to use the DI with Unity is to configure the container either at design time or runtime. There is no performance difference between the two configuring approaches, but the design time configuration is more flexible and maintainable. If an application has similar components targeting for different data sources or service layers, using the design time configuration can switch between desired assemblies from the XML config file without recompiling and deploying the application projects.
Since the database connection string is passed as the parameter in the constructor of the UOW object, code in runtime configuration can directly retrieve the connection string value from the normal place in the web.config file. However, the design time configuration cannot get the connection string using the same way. The alternative is to use a placeholder for the Value
attribute in the Unity.config file. In the runtime, the code can then get the real connection string value from the web.config and replace the placeholder in the constructor of the SM.Store.Service.DAL.StoreDataModelUnitOfWork
class.
The sample application demonstrates the use of both configuration options in the code shown below.
The runtime configuration code in the DIRegister.cs file:
public static void RegisterTypes(UnityContainer Container)
{
string connString = ConfigurationManager.ConnectionStrings
["StoreDataEntities"].ConnectionString;
Container.RegisterType<DAL.IUnitOfWork,
DAL.StoreDataModelUnitOfWork>(new PerResolveLifetimeManager(),
new InjectionConstructor(connString));
Container.RegisterType<BLL.IProductBS, BLL.ProductBS>();
Container.RegisterType<DAL.IProductRepository, DAL.ProductRepository>();
Container.RegisterType<BLL.ICategoryBS, BLL.CategoryBS>();
Container.RegisterType<DAL.IGenericRepository<Entities.Category>,
DAL.GenericRepository<Entities.Category>>();
}
The method is called from the Global.asax.cs when the service starts.
protected void Application_Start(object sender, EventArgs e)
{
DIRegister.RegisterTypes(DIFactoryForRuntime.Container);
}
The design time configuration code in the Unity.config file:
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<assembly name="SM.Store.Service.DAL"/>
<assembly name="SM.Store.Service.BLL"/>
<alias alias="Category"
type="SM.Store.Service.Entities.Category, SM.Store.Service.Entities" />
<container>
<register type="SM.Store.Service.DAL.IUnitOfWork"
mapTo="SM.Store.Service.DAL.StoreDataModelUnitOfWork">
<lifetime type="singleton" />
<constructor>
<param name="connectionString" value="{connectionString}" />
</constructor>
</register>
<register type="SM.Store.Service.BLL.IProductBS"
mapTo="SM.Store.Service.BLL.ProductBS"/>
<register type="SM.Store.Service.DAL.IProductRepository"
mapTo="SM.Store.Service.DAL.ProductRepository"/>
<register type="SM.Store.Service.BLL.ICategoryBS"
mapTo="SM.Store.Service.BLL.CategoryBS"/>
<register type="SM.Store.Service.DAL.IGenericRepository[Category]"
mapTo="SM.Store.Service.DAL.GenericRepository[Category]"/>
</container>
</unity>
The container is registered from the web.config file when the service starts.
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration"/>
</configSections>
<unity configSource="Unity.config"/>
. . .
</configuration>
The code for replacing the placeholder with the real connection string in the SM.Store.Service.DAL.StoreDataModelUnitOfWork
class:
public StoreDataModelUnitOfWork(string connectionString)
{
if (connectionString == "{connectionString}")
connectionString = ConfigurationManager.ConnectionStrings
["StoreDataEntities"].ConnectionString;
this.context = new StoreDataEntities(connectionString);
}
For illustrations in the sample application, some service methods call the Unity container for component instances using the runtime configurations whereas others using the design time configurations. In the real world, of course, you may only use one kind of configurations within the same application.
Disabling Lazy Loading
By default, the Lazy Loading for the data context is enabled. This may not be the optimal setting for the data access tier wrapped in the web service since there are multiple hits to the database server during the entity-contract type conversions before the serializations for contract objects. It could slow the type conversion processes. We need to disable the lazy loading for the data context by right-clicking any blank area of the EF Designer, select the Properties, and change the Lazy Loading Enabled to False, and then save the EDMX file. We can then use following approaches in the code.
- Eager loading that includes the child entities if desired. The
GetProductList()
and GetProductByCategoryId()
methods in the SM.Store.Service.DAL.ProductRepository
class use the eager loading as examples. - Joining linked entities in the LINQ to Entity query and returning data with a custom type. The
GetFullProducts()
method in the SM.Store.Service.DAL.ProductRepository
class shows this approach. - Calling stored procedures and returning data with an EF complex type. The sample application includes a more advanced EF stored procedure mappings for returning multiple result sets.
These alternative approaches will also be discussed with related topics in the following sections.
Getting Paginated and Sorted Data Sets
Implementing the EF server-side paging and sorting needs constructions of appropriate LINQ queries. The sorting is a must when doing the paging.
The most concerned issue on the sorting is how to return records sorted by a child entity property. This can be done by searching expression tree nodes to find the property of the child entity and then build the OrderBy
statement of the query. Below is the method in the SM.Store.Service.Common.GenericSorterPager
class for building this part of the query.
public static IOrderedQueryable<T> AsSortedQueryable<T, TSortByType>
(IQueryable<T> source, string sortBy, SortDirection sortDirection)
{
var param = Expression.Parameter(typeof(T), "item");
Expression parent = param;
string[] props = sortBy.Split(".".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
foreach (string prop in props)
{
parent = Expression.Property(parent, prop);
}
var sortExpression = Expression.Lambda<Func<T, TSortByType>>(parent, param);
switch (sortDirection)
{
case SortDirection.Descending:
return source.OrderByDescending<T, TSortByType>(sortExpression);
default:
return source.OrderBy<T, TSortByType>(sortExpression);
}
}
The most concerned issue on the paging is how to consolidate calls for data sets and a total count value. The EF paging usually needs two calls for the requested data, one for data sets and the other for a total count value which is needed for populating the grid or table display structures in the client UI application. Two calls for returning paged and count data may have an impact on the performance for large databases. This issue can be resolved using the code in the GetSortedPagedList()
method in the SM.Store.Service.Common.GenericSorterPager
class.
var pagedGroup = from sel in sortedPagedQuery
select new PagedResultSet<T>()
{
PagedData = sel,
TotalCount = source.Count()
};
List<PagedResultSet<T>> pagedResultSet = pagedGroup.AsParallel().ToList();
IEnumerable<T> pagedList = new List<T>();
if (pagedResultSet.Count() > 0)
{
totalCount = pagedResultSet.First().TotalCount;
pagedList = pagedResultSet.Select(s => s.PagedData);
}
pagedResultSet = null;
return pagedList.ToList();
The SQL query generated from the EF behind the scene by checking the <code>pagedGrou
p variable value is shown below. It actually uses the Cross Join
to get the total count value and attach it to each row. Although this is a little redundant, it’s not the issue since there is only limited number of rows returned in the paged data sets (10 rows in our example).
SELECT TOP (10)
[Join3].[ProductID] AS [ProductID],
[Join3].[C1] AS [C1],
[Join3].[ProductName] AS [ProductName],
[Join3].[CategoryID] AS [CategoryID],
[Join3].[UnitPrice] AS [UnitPrice],
[Join3].[StatusCode] AS [StatusCode],
[Join3].[Description] AS [Description],
[Join3].[A1] AS [C2]
FROM ( SELECT [Distinct1].[ProductID] AS [ProductID], _
[Distinct1].[ProductName] AS [ProductName], _
[Distinct1].[CategoryID] AS [CategoryID], [Distinct1].[UnitPrice] AS [UnitPrice], _
[Distinct1].[StatusCode] AS [StatusCode], [Distinct1].[Description] AS [Description], _
[Distinct1].[C1] AS [C1], [GroupBy1].[A1] AS [A1], row_number() _
OVER (ORDER BY [Distinct1].[ProductName] ASC) AS [row_number]
FROM (SELECT DISTINCT
[Extent1].[ProductID] AS [ProductID],
[Extent1].[ProductName] AS [ProductName],
[Extent1].[CategoryID] AS [CategoryID],
[Extent1].[UnitPrice] AS [UnitPrice],
[Extent1].[StatusCode] AS [StatusCode],
[Extent2].[Description] AS [Description],
1 AS [C1]
FROM [dbo].[Product] AS [Extent1]
INNER JOIN [dbo].[ProductStatusType] AS [Extent2] _
ON [Extent1].[StatusCode] = [Extent2].[StatusCode] ) AS [Distinct1]
CROSS JOIN (SELECT
COUNT(1) AS [A1]
FROM ( SELECT DISTINCT
[Extent3].[ProductID] AS [ProductID],
[Extent3].[ProductName] AS [ProductName],
[Extent3].[CategoryID] AS [CategoryID],
[Extent3].[UnitPrice] AS [UnitPrice],
[Extent3].[StatusCode] AS [StatusCode],
[Extent4].[Description] AS [Description]
FROM [dbo].[Product] AS [Extent3]
INNER JOIN [dbo].[ProductStatusType] AS [Extent4] _
ON [Extent3].[StatusCode] = [Extent4].[StatusCode]
) AS [Distinct2] ) AS [GroupBy1]
) AS [Join3]
WHERE [Join3].[row_number] > 0
ORDER BY [Join3].[ProductName] ASC
Note that the pagedGroup
query doesn’t return the data from any child entity when using the eager loading for the base source sortedPagedQuery
passed in the generic IQueriable<T>
type. It works for the lazy loading, but additional calls are also needed to get the data from child entities. Since we disable the lazy loading for the data context, we need to make a separate call to obtain the total count value if an eager loading is used. The code for the GetSortedPagedList()
method handles calls from queries with both eager loading and no loading of child entities.
public static IList<T> GetSortedPagedList<T>(IQueryable<T> source,
PaginationRequest paging, out int totalCount, ChildLoad childLoad = ChildLoad.None)
{
. . .
if (childLoad == ChildLoad.Include)
{
totalCount = source.Count();
}
IQueryable<T> sortedPagedQuery = GetSortedPagedQuerable<T>(source, paging);
if (childLoad == ChildLoad.None)
{
[Code of one-call query shown before]
}
else
{
return sortedPagedQuery.ToList();
}
}
Object Type Conversion and EF Query Scenarios
The SOAP based web services communicate with the service consumers through serialization and deserialization processes for contract objects. However, the entity type objects are used in all BLL and DAL. The entity-contract type conversions, and vice versa, are conducted during each request and response between services and clients. These conversions also add overheads to the service data operations, especially in these situations:
- Retrieving a data set containing child entities when the lazy loading is enabled. Calls to the database for child entity data occur during the mapping processes in this case, which should be avoided in a data access web service.
- Using eager loading for a data set including child entities when the large number of rows is returned and multiple, or even multiple levels of, child entities are included. Converters need extra work to search and map those child entity properties.
Options on mapping properties between object types can be auto, semi-auto, and manual mappings. We can use the AutoMapper library to do the auto and the semi-auto mappings in a generic style whereas the manual conversion needs one-to-one property value assignment between source and destination objects. The data type conversion approaches, although performed in the service endpoint methods, are also related to EF queries used in the DAL. Generally, using auto mapping eases the development and maintenance tasks but reduces the performance. Here are several mapping scenarios shown in the sample application.
Entities.Product ent = ProductBs.GetProductById(request.Id);
IBaseConverter<Entities.Product, DataContracts.Product>
convtResult = new AutoMapConverter<Entities.Product, DataContracts.Product>();
resp.Product = convtResult.ConvertObject(ent);
This scenario is also used for all cases converting data contract objects back to entity objects for Insert
and Update
operations which are all single entity based.
ProductAutoMapConverter convtResult = new ProductAutoMapConverter();
List<DataContracts.ProductCM> dcList = convtResult.ConvertObjectCollection(productList);
The code in the ProductAutoMapConverter
class manually maps the child entity property ProductStatusType.Description
to the StatusDescription
property of the contract object so that the data value of StatusDescription
field can be shown in the returned data list. Full auto mapping processes are used for all other properties.
public class ProductAutoMapConverter : AutoMapConverter<Entities.Product,
DataContracts.ProductCM>
{
public ProductAutoMapConverter()
{
AutoMapper.Mapper.CreateMap<Entities.Product, DataContracts.ProductCM>()
.ForMember(dest => dest.StatusDescription, _
opt => opt.MapFrom(src => src.ProductStatusType.Description));
}
}
public override DataContracts.ProductCM ConvertObject(Entities.Product ent)
{
return new DataContracts.ProductCM()
{
ProductID = ent.ProductID,
ProductName = ent.ProductName,
CategoryID = ent.CategoryID != null ? ent.CategoryID.Value : 0,
UnitPrice = ent.UnitPrice,
StatusCode = ent.StatusCode,
StatusDescription = ent.ProductStatusType != null ?
ent.ProductStatusType.Description : string.Empty
};
}
IQueryable<Entities.ProductCM> query = this.UnitOfWork.Context.Products
.Join(this.UnitOfWork.Context.ProductStatusTypes,
p => p.StatusCode, s => s.StatusCode,
(p, s) => new Entities.ProductCM
{
ProductID = p.ProductID,
ProductName = p.ProductName,
CategoryID = p.CategoryID,
UnitPrice = p.UnitPrice,
StatusCode = p.StatusCode,
StatusDescription = s.Description
});
The EF query takes care of joining the underlying parent and child tables in the database and returns data with all fields from parent and child tables as properties of the custom type. We can then just use the simple auto mapping for the ProductCM
to its contract type counterpart.
IBaseConverter<Entities.ProductCM, DataContracts.ProductCM> convtResult =
new AutoMapConverter<Entities.ProductCM, DataContracts.ProductCM>();
Based on my experiences, this is the best scenario for EF queries and data type conversions in a data access WCF web service since the DAL only returns data in a collection with just a simple object type, not including any other child or linked object. We then avoid digging into the child entities to map members during the entity-contract type conversions. In this scenario, we can also ignore whether the lazy or eager loading is used or not.
- Using simple auto mapping for returned data containing no child entity. The
GetProductById()
method in the DAL makes a simple query call to get an entity data record without any property from child entities. - Using the semi-auto or custom configured mapping for returned data including any property from child entities (eager loading in our case). This approach is demonstrated in the
GetProductList()
service method. - Using pure manual mapping. The
GetProductByCategoryId()
service method uses the pure manual mapping approach. It calls the ProductManualConverter
class which does the assignments for each property returned. - Using simple auto mapping with optimized EF queries. This needs a custom POCO object similar to the
ViewModel
object that includes all needed properties from child entities. In our case, it’s the ProductCM
class in the SM.Store.Service.Entities
project. Inside the ProductReporsitory.GetFullProducts()
method in the DAL, the EF query is defined as shown blow.
Returning Multiple Result Sets from Database Stored Procedures
The sample app shows how to use this feature with EF designer Function Import mappings. This part is only included in the source code for VS 2012/2013 since it is only supported by .NET Framework 4.5. The implementation details, particularly for manually editing function import mappings, are described in my previous article.
For the equivalent returning complex types in the entity side, the response data contract object and collections are added into the web service project.
[DataContract(Namespace = Constants.NAMESPACE)]
public class CategoriesProductsResponse
{
[DataMember(IsRequired = false, Order = 0)]
public CategoriesCM Categories { get; set; }
[DataMember(IsRequired = false, Order = 1)]
public ProductsCM Products { get; set; }
}
[CollectionDataContract(Namespace = Constants.NAMESPACE,
Name = "CategoriesCM", ItemName = "CategoryCM")]
public class CategoriesCM : List<DataContracts.CategoryCM> { }
[CollectionDataContract(Namespace = Constants.NAMESPACE,
Name = "ProductsCM", ItemName = "ProductCM")]
public class ProductsCM : List<DataContracts.ProductCM> { }
The service will then return the multiple result sets after the type conversions. Since the complex types already include all wanted fields and no field mapping from any child data structure is needed, we can directly use the AutoMapper for simple mappings to achieve the optimal performance and maintainability.
public DataContracts.CategoriesProductsResponse
GetCategoriesAndProducts(DataContracts.QueryByStringRequest request)
{
DataContracts.CategoriesProductsResponse resp =
new DataContracts.CategoriesProductsResponse();
resp.Categories = new DataContracts.CategoriesCM();
resp.Products = new DataContracts.ProductsCM();
ICategoryBS categoryBS = DIFactoryForDesigntime.GetInstance<ICategoryBS>();
Entities.CategoriesProducts ent = categoryBS.GetCategoriesAndProducts();
IBaseConverter<Entities.Category_Result, DataContracts.CategoryCM> convtResult1 =
new AutoMapConverter<Entities.Category_Result, DataContracts.CategoryCM>();
List<DataContracts.CategoryCM> dcList1 =
convtResult1.ConvertObjectCollection(ent.Categories);
IBaseConverter<Entities.Product_Result, DataContracts.ProductCM> convtResult2 =
new AutoMapConverter<Entities.Product_Result, DataContracts.ProductCM>();
List<DataContracts.ProductCM> dcList2 = convtResult2.ConvertObjectCollection(ent.Products);
resp.Categories.AddRange(dcList1);
resp.Products.AddRange(dcList2);
return resp;
}
Running and Testing Service from Clients
In the local development environment, the sample web service runs from the development server with Visual Studio 2010 and IIS Express with Visual Studio 2012/2013. To test the service locally from any external client application, you firstly need to start the web service by running the browser pointing to a service endpoint URL. The easiest way to do this is to execute the context-menu command View in Browser on the endpoint file (*.svc) from the Visual Studio Solution Explorer. You can then test the service using the client application or tools, such as WcfTestClient.exe (in C:\Program Files (x86)\Microsoft Visual Studio [version_ number]\Common7\IDE) or SoapUI. If opening the WcfTestClient
, adding the "http://localhost:23066/Services/ProductService.svc" endpoint URL into it, then selecting and invoking the GetProductByCategoryId()
service method, the resulted screen should be like this:
The sample application also includes a unit test project in the client side. It sets the service references items CategorySvcRef
and ProductSvcRe
f as the client proxies pointing to the service endpoints. The project has many advantages as a test client for the service:
- Automatic service starting. When setting the test project as the starting project and running it in the same Visual Studio solution, you don’t have to manually start the service from a browser before running the test since the service will automatically be running there.
- Conducting manual or automated unit tests.
- Used for easy debugging. From the code lines executed in the test class, we can step into the code of any project in the service solution.
- Checking details of returned data. When placing a breakpoint on a line after returning the data, we can see all data details from the client side in the Tooltip, Autos, or Locals window.
The Task-based Asynchronous Pattern (TAP) is the recommended asynchronous design pattern for new development. With .NET Framework 4.5, the structure and syntax are simplified to implement the TAP. To call a service method asynchronously, we don’t have to do anything in the server side. All we need is to enable the Allow generation of asynchronous operations and Generate task-based operations on the Service Reference Settings dialog when adding new, or updating/configuring existing, service references for the client applications. The corresponding methods with suffix Async
and returning awaitable Task
type for the TAP operations are then automatically added into the client proxy.
The TestClientConsole
project in the sample application contains code to call the GetFullProductsAsync()
service method wrapped by the GetFullProductsTaskAsync()
method with the async
keyword which opens another thread to process the data retrieval asynchronously. The Console.Write()
code line is physically located after calling the GetFullProductsTaskAsync()
in the main thread but it continues to run and displays the text in the Console window before the data retrieval completes and displays.
static void Main(string[] args)
{
GetFullProductsTaskAsync();
Console.Write("This code line after async call in main thread
\r\nbut was executed before returning async data\r\n");
Console.ReadKey();
}
private async static void GetFullProductsTaskAsync()
{
. . .
Task<ProductListResponse> taskResp = client.GetFullProductsAsync(inputItem);
var resp = await taskResp;
Console.WriteLine("");
Console.WriteLine("Products: (total {0}, listed {1})",
resp.TotalCount.ToString(), resp.Products.Count.ToString());
foreach (var item in resp.Products)
{
Console.WriteLine("");
Console.WriteLine("Name: {0}", item.ProductName);
Console.WriteLine("Price: ${0}", item.UnitPrice.ToString());
}
}
Many areas of a WCF and EF data access web service could be improved or enhanced to make the application more robust and valuable. I hope that the article is helpful to developers who are interested in, or working, on the data tier service applications.
History
- 21st November, 2013: Initial version